import json
import subprocess
from pathlib import Path


MODULE = Path(__file__).parents[1] / "frontend" / "issue-filing-review.js"


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


def test_review_requires_one_confirmation_and_preserves_the_reviewed_payload():
    script = f"""
const createReview = require({json.dumps(str(MODULE))});
function target() {{
  const listeners = {{}};
  return {{
    disabled:false, hidden:true, textContent:'', children:[],
    addEventListener:(name, fn)=>listeners[name]=fn,
    dispatch:(name, event={{}})=>listeners[name]?.({{preventDefault(){{}}, ...event}}),
    focus(){{this.focused=true;}}, replaceChildren(...items){{this.children=items;}},
    setAttribute(){{}}, appendChild(item){{this.children.push(item);}},
  }};
}}
const sheet=target(), confirm=target(), back=target(), trigger=target(), evidence=target();
const documentRef={{
  createElement:tag=>({{tag, textContent:'', children:[], appendChild(item){{this.children.push(item);}}}}),
  addEventListener:()=>{{}},
}};
const admitted=[];
const review=createReview({{
  sheet, confirmButton:confirm, backButton:back, evidenceList:evidence,
  repository:target(), intent:target(), title:target(), body:target(), metadata:target(),
  document:documentRef,
  onConfirm:async payload=>{{ admitted.push(payload); return {{ok:true}}; }},
}});
const payload={{
  draft:{{repository:'stackchain/dashboard',title:'Ship it',body:'Full note',labels:['P1'],
    milestone:{{title:'Sprint'}},dueDate:'2026-08-20',attachments:[
      {{filename:'first.png',note:'Before'}},{{filename:'second.png',note:'After'}}
    ]}},
  intent:'create-and-start',
}};
review.open(payload, trigger);
payload.draft.title='Mutated outside';
confirm.dispatch('click');
confirm.dispatch('click');
setImmediate(()=>process.stdout.write(JSON.stringify({{
  admissions:admitted.length,
  reviewedTitle:admitted[0].draft.title,
  evidence:evidence.children.map(item=>item.textContent),
  hidden:sheet.hidden,
  triggerFocused:trigger.focused || false,
}})));
"""

    result = run_node(script)

    assert result == {
        "admissions": 1,
        "reviewedTitle": "Ship it",
        "evidence": ["1. first.png — Before", "2. second.png — After"],
        "hidden": True,
        "triggerFocused": True,
    }


def test_review_lists_selected_blockers_in_filing_order():
    script = f"""
const createReview=require({json.dumps(str(MODULE))});
function target(){{const listeners={{}};return{{hidden:true,disabled:false,textContent:'',children:[],addEventListener:(n,f)=>listeners[n]=f,replaceChildren(...items){{this.children=items;}},focus(){{}}}};}}
const blockerList=target();
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),blockerList,
  repository:target(),intent:target(),title:target(),body:target(),metadata:target(),
  document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
review.open({{draft:{{repository:'o/r',title:'Blocked',blockers:[
  {{repository:'o/api',number:7,title:'API ready'}},{{repository:'o/web',number:8,title:'Web ready'}}
]}},intent:'create-and-assign'}},target());
process.stdout.write(JSON.stringify(blockerList.children.map(item=>item.textContent)));
"""
    assert run_node(script) == ["o/api #7 — API ready", "o/web #8 — Web ready"]


def test_review_names_the_selected_initial_owner():
    script = f"""
const createReview=require({json.dumps(str(MODULE))});
function target(){{return{{hidden:true,disabled:false,textContent:'',addEventListener:()=>{{}},replaceChildren:()=>{{}},focus:()=>{{}}}};}}
const metadata=target();
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),
  repository:target(),intent:target(),title:target(),body:target(),metadata,
  document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
review.open({{draft:{{repository:'o/r',title:'Delegate',assignee:'alex',assigneeName:'Alex'}},intent:'create-and-assign'}},target());
process.stdout.write(JSON.stringify({{metadata:metadata.textContent}}));
"""
    assert run_node(script)["metadata"].endswith("Owner: Alex (@alex)")


def test_review_names_explicit_no_owner():
    script = f"""
const createReview=require({json.dumps(str(MODULE))});
function target(){{return{{hidden:true,disabled:false,textContent:'',addEventListener:()=>{{}},replaceChildren:()=>{{}},focus:()=>{{}}}};}}
const metadata=target(),intent=target();
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),
  repository:target(),intent,title:target(),body:target(),metadata,
  document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
review.open({{draft:{{repository:'o/r',title:'Backlog capture',unassigned:true}},intent:'create-and-assign'}},target());
process.stdout.write(JSON.stringify({{metadata:metadata.textContent,intent:intent.textContent}}));
"""
    output = run_node(script)
    assert output["metadata"].endswith("Owner: No owner")
    assert output["intent"] == "Create unassigned"


def test_review_names_the_repository_issue_type():
    script = f"""
const createReview=require({json.dumps(str(MODULE))});
function target(){{return{{hidden:true,disabled:false,textContent:'',addEventListener:()=>{{}},replaceChildren:()=>{{}},focus:()=>{{}}}};}}
const issueType=target();
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),
  repository:target(),intent:target(),title:target(),body:target(),metadata:target(),issueType,
  document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
review.open({{draft:{{repository:'o/r',title:'Crash',templateName:'Bug report'}},intent:'create-and-assign'}},target());
process.stdout.write(JSON.stringify({{issueType:issueType.textContent}}));
"""
    assert run_node(script)["issueType"] == "Bug report"


def test_create_and_start_review_shows_estimate_and_projected_capacity():
    script = f"""
const createReview=require({json.dumps(str(MODULE))});
function target(){{return{{hidden:true,disabled:false,textContent:'',addEventListener:()=>{{}},replaceChildren:()=>{{}},focus:()=>{{}}}};}}
const metadata=target();
const review=createReview({{sheet:target(),confirmButton:target(),backButton:target(),evidenceList:target(),
  repository:target(),intent:target(),title:target(),body:target(),metadata,
  document:{{createElement:()=>target(),addEventListener:()=>{{}}}},onConfirm:async()=>{{}}}});
review.open({{draft:{{repository:'o/r',title:'Sized work',estimateMinutes:45,
  todayCapacity:{{remainingMinutes:60,projectedMinutes:15}}}},intent:'create-and-start'}},target());
process.stdout.write(JSON.stringify({{metadata:metadata.textContent}}));
"""
    metadata = run_node(script)["metadata"]
    assert "Estimate: 45 min" in metadata
    assert "Today after start: 15 min free" in metadata


def test_escape_returns_to_the_unchanged_issue_form():
    script = f"""
const createReview = require({json.dumps(str(MODULE))});
function target() {{
  const listeners={{}};
  return {{hidden:true,disabled:false,textContent:'',children:[],
    addEventListener:(name,fn)=>listeners[name]=fn,
    replaceChildren(...items){{this.children=items;}},
    focus(){{this.focused=true;}}, dispatch:(name,event)=>listeners[name]?.(event)}};
}}
const listeners={{}}, documentRef={{
  createElement:()=>({{textContent:''}}),
  addEventListener:(name,fn)=>listeners[name]=fn,
  dispatch:(name,event)=>listeners[name]?.(event),
}};
const sheet=target(), trigger=target();
const review=createReview({{
  sheet,confirmButton:target(),backButton:target(),evidenceList:target(),
  repository:target(),intent:target(),title:target(),body:target(),metadata:target(),
  document:documentRef,onConfirm:async()=>{{}},
}});
const payload={{draft:{{repository:'o/r',title:'Original',body:'Note'}},intent:'create-and-assign'}};
review.open(payload,trigger);
documentRef.dispatch('keydown',{{key:'Escape',preventDefault(){{this.prevented=true;}}}});
process.stdout.write(JSON.stringify({{hidden:sheet.hidden,focused:trigger.focused||false,title:payload.draft.title}}));
"""

    assert run_node(script) == {"hidden": True, "focused": True, "title": "Original"}


def test_review_renders_exact_evidence_bytes_and_selects_the_preview():
    script = f"""
const createReview = require({json.dumps(str(MODULE))});
function target(tag='div') {{
  const listeners={{}}, attributes={{}};
  return {{tag,hidden:true,disabled:false,textContent:'',children:[],src:'',alt:'',
    addEventListener:(name,fn)=>listeners[name]=fn,
    dispatch:(name,event={{}})=>listeners[name]?.({{preventDefault(){{}},currentTarget:this,...event}}),
    replaceChildren(...items){{this.children=items;}}, appendChild(item){{this.children.push(item);}},
    setAttribute(name,value){{attributes[name]=String(value);}}, getAttribute:name=>attributes[name],
    focus(){{this.focused=true;}}}};
}}
const documentRef={{createElement:tag=>target(tag),addEventListener:()=>{{}}}};
const evidence=target(), preview=target(), image=target('img');
const position=target(), filename=target(), note=target(), empty=target();
const created=[];
const review=createReview({{
  sheet:target(),confirmButton:target(),backButton:target(),evidenceList:evidence,
  evidencePreview:preview,evidenceImage:image,evidencePosition:position,
  evidenceFilename:filename,evidenceNote:note,evidenceEmpty:empty,
  repository:target(),intent:target(),title:target(),body:target(),metadata:target(),
  document:documentRef,createObjectURL:blob=>{{const url='blob:review-'+blob.size;created.push(url);return url;}},
  revokeObjectURL:()=>{{}},onConfirm:async()=>{{}},
}});
review.open({{draft:{{repository:'o/r',title:'Privacy check',attachments:[
  {{filename:'redacted.png',contentType:'image/png',blob:new Blob(['flattened-redaction']),note:'Secrets hidden'}},
  {{filename:'legacy.webp',contentType:'image/webp',data:'bGVnYWN5',note:'Expected result'}}
]}},intent:'create-and-assign'}},target());
const firstButton=evidence.children[0].children[0];
const secondButton=evidence.children[1].children[0];
secondButton.dispatch('click');
process.stdout.write(JSON.stringify({{
  created,thumbnailSources:[firstButton.children[0].src,secondButton.children[0].src],
  labels:[firstButton.getAttribute('aria-label'),secondButton.getAttribute('aria-label')],
  selected:[firstButton.getAttribute('aria-pressed'),secondButton.getAttribute('aria-pressed')],
  preview:{{hidden:preview.hidden,src:image.src,alt:image.alt,position:position.textContent,
    filename:filename.textContent,note:note.textContent}},emptyHidden:empty.hidden,
}}));
"""

    result = run_node(script)

    assert result == {
        "created": ["blob:review-19"],
        "thumbnailSources": [
            "blob:review-19",
            "data:image/webp;base64,bGVnYWN5",
        ],
        "labels": [
            "Preview evidence 1 of 2: redacted.png",
            "Preview evidence 2 of 2: legacy.webp",
        ],
        "selected": ["false", "true"],
        "preview": {
            "hidden": False,
            "src": "data:image/webp;base64,bGVnYWN5",
            "alt": "Evidence 2 of 2: legacy.webp",
            "position": "Evidence 2 of 2",
            "filename": "legacy.webp",
            "note": "Expected result",
        },
        "emptyHidden": True,
    }


def test_failed_filing_keeps_preview_and_success_releases_blob_url_once():
    script = f"""
const createReview = require({json.dumps(str(MODULE))});
function target() {{
  const listeners={{}};
  return {{hidden:true,disabled:false,textContent:'',children:[],src:'',alt:'',
    addEventListener:(name,fn)=>listeners[name]=fn,
    dispatch:(name,event={{}})=>listeners[name]?.({{preventDefault(){{}},...event}}),
    replaceChildren(...items){{this.children=items;}},appendChild(item){{this.children.push(item);}},
    setAttribute(){{}},focus(){{this.focused=true;}}}};
}}
const documentRef={{createElement:()=>target(),addEventListener:()=>{{}}}};
const sheet=target(), confirm=target(), trigger=target(), image=target(), preview=target();
const revoked=[];
let attempts=0;
const review=createReview({{
  sheet,confirmButton:confirm,backButton:target(),evidenceList:target(),evidencePreview:preview,
  evidenceImage:image,evidencePosition:target(),evidenceFilename:target(),evidenceNote:target(),evidenceEmpty:target(),
  repository:target(),intent:target(),title:target(),body:target(),metadata:target(),status:target(),
  document:documentRef,createObjectURL:blob=>'blob:attempt-'+blob.size,
  revokeObjectURL:url=>revoked.push(url),
  onConfirm:async()=>{{attempts+=1;if(attempts===1)throw new Error('Network unavailable');}},
}});
review.open({{draft:{{repository:'o/r',title:'Verify',attachment:{{filename:'one.png',blob:new Blob(['one'])}}}},intent:'create-and-assign'}},trigger);
confirm.dispatch('click');
setImmediate(()=>{{
  const afterFailure={{hidden:sheet.hidden,previewHidden:preview.hidden,src:image.src,disabled:confirm.disabled,revoked:[...revoked]}};
  confirm.dispatch('click');
  setImmediate(()=>process.stdout.write(JSON.stringify({{
    attempts,afterFailure,afterSuccess:{{hidden:sheet.hidden,triggerFocused:trigger.focused||false,revoked}}
  }})));
}});
"""

    assert run_node(script) == {
        "attempts": 2,
        "afterFailure": {
            "hidden": False,
            "previewHidden": False,
            "src": "blob:attempt-3",
            "disabled": False,
            "revoked": [],
        },
        "afterSuccess": {
            "hidden": True,
            "triggerFocused": True,
            "revoked": ["blob:attempt-3"],
        },
    }


def test_create_actions_review_the_complete_payload_before_durable_admission():
    root = Path(__file__).parents[1]
    html = (root / "frontend" / "index.html").read_text()
    dashboard = (root / "frontend" / "dashboard.js").read_text()
    css = (root / "frontend" / "dashboard.css").read_text()
    bundle = (root / "src" / "frontend_bundle.py").read_text()

    assert 'id="issue-filing-review" role="dialog" aria-modal="true"' in html
    assert 'id="issue-filing-review-repository"' in html
    assert 'id="issue-filing-review-intent"' in html
    assert 'id="issue-filing-review-title"' in html
    assert 'id="issue-filing-review-body"' in html
    assert 'id="issue-filing-review-evidence"' in html
    assert 'id="issue-filing-review-evidence-empty"' in html
    assert 'id="issue-filing-review-evidence-preview"' in html
    assert 'id="issue-filing-review-evidence-image"' in html
    assert 'id="issue-filing-review-evidence-position"' in html
    assert 'id="issue-filing-review-evidence-filename"' in html
    assert 'id="issue-filing-review-evidence-note"' in html
    assert 'id="back-to-issue-edit"' in html
    assert 'id="confirm-issue-filing"' in html
    assert '<script src="static/issue-filing-review.js"></script>' in html

    submit_handler = dashboard.split(
        "qs('#create-issue-form').addEventListener('submit'", 1
    )[1].split("qs('#close-issue-sheet').addEventListener", 1)[0]
    assert "filingReview.open" in submit_handler
    assert "issueOutbox.enqueueDurably" not in submit_handler
    assert "async function admitReviewedIssue" in dashboard
    assert "evidencePreview: qs('#issue-filing-review-evidence-preview')" in dashboard
    assert "evidenceImage: qs('#issue-filing-review-evidence-image')" in dashboard
    assert "createObjectURL: blob => URL.createObjectURL(blob)" in dashboard
    assert "revokeObjectURL: url => URL.revokeObjectURL(url)" in dashboard
    assert "issueOutbox.enqueueDurably" in dashboard.split(
        "async function admitReviewedIssue", 1
    )[1].split("qs('#create-issue-form').addEventListener", 1)[0]

    assert ".issue-filing-review" in css
    assert "max-height:100dvh" in css
    assert "overflow-x:hidden" in css
    assert "env(safe-area-inset-bottom)" in css
    assert ".issue-filing-evidence-preview" in css
    assert ".issue-filing-evidence-selector" in css
    assert "min-height:44px" in css.split(".issue-filing-evidence-selector", 1)[1]
    assert '"static/issue-filing-review.js"' in bundle


def test_duplicate_acknowledgement_preserves_the_selected_filing_intent():
    dashboard = (Path(__file__).parents[1] / "frontend" / "dashboard.js").read_text()

    assert "let pendingIssueFilingIntent = 'create-and-assign';" in dashboard
    assert "if (event.submitter) pendingIssueFilingIntent" in dashboard
    assert "const intent = pendingIssueFilingIntent;" in dashboard
    assert "pendingIssueFilingIntent = 'create-and-assign';" in dashboard.split(
        "async function admitReviewedIssue", 1
    )[1].split("qs('#create-issue-form').addEventListener", 1)[0]
