import json
import subprocess
from pathlib import Path


ROOT = Path(__file__).parents[1]
EDITOR = ROOT / "frontend" / "issue-evidence-editor.js"
ATTACHMENT = ROOT / "frontend" / "issue-attachment.js"
INDEX = ROOT / "frontend" / "index.html"
CSS = ROOT / "frontend" / "dashboard.css"
SERVICE_WORKER = ROOT / "frontend" / "service-worker.js"


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


def test_editor_crops_redacts_undoes_and_flattens_derivative():
    script = f"""
const editor = require({json.dumps(str(EDITOR))});
const model = editor.createModel({{width:400,height:300}});
model.setCrop({{x:40,y:30,width:240,height:180}});
model.addRedaction({{x:70,y:60,width:80,height:40}});
model.addRedaction({{x:180,y:120,width:50,height:30}});
model.undo();
const calls=[];
const canvas={{width:0,height:0,getContext:()=>({{
  drawImage:(...args)=>calls.push(['draw',...args.slice(1)]),
  fillRect:(...args)=>calls.push(['fill',...args]),
  set fillStyle(value){{calls.push(['color',value]);}}
}}),toBlob:(callback,type)=>callback(new Blob(['flattened'],{{type}}))}};
(async()=>{{
 const result=await editor.flatten({{source:{{}},model,canvas,name:'private.png',type:'image/png'}});
 process.stdout.write(JSON.stringify({{snapshot:model.snapshot(),calls,width:canvas.width,height:canvas.height,
   name:result.name,type:result.type,text:await result.text()}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = run_node(script)
    assert output["snapshot"] == {
        "crop": {"x": 40, "y": 30, "width": 240, "height": 180},
        "redactions": [{"x": 70, "y": 60, "width": 80, "height": 40}],
        "annotations": [],
    }
    assert output["width"] == 240
    assert output["height"] == 180
    assert output["calls"] == [
        ["draw", 40, 30, 240, 180, 0, 0, 240, 180],
        ["color", "#000000"],
        ["fill", 30, 30, 80, 40],
    ]
    assert output["name"] == "private.png"
    assert output["type"] == "image/png"
    assert output["text"] == "flattened"


def test_applying_edit_replaces_only_selected_blob_and_preserves_note_and_order():
    script = f"""
const attachment=require({json.dumps(str(ATTACHMENT))});
let id=0;
const image=name=>{{const blob=new Blob([name],{{type:'image/png'}});blob.name=name;return blob;}};
const controller=attachment.create({{maxFiles:5,createOperationId:()=>`edit-${{++id}}`,upload:async()=>({{markdown:'ok'}})}});
['one.png','private.png','three.png'].forEach(name=>controller.select(image(name)));
controller.setNote(1,'Token hidden here');
controller.replace(1,image('private-edited.png'));
(async()=>{{
 const values=await controller.serialize();
 process.stdout.write(JSON.stringify({{values:await Promise.all(values.map(async value=>({{name:value.filename,note:value.note||'',text:await value.blob.text()}}))),state:controller.state()}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = run_node(script)
    assert output["values"] == [
        {"name": "one.png", "note": "", "text": "one.png"},
        {"name": "private-edited.png", "note": "Token hidden here", "text": "private-edited.png"},
        {"name": "three.png", "note": "", "text": "three.png"},
    ]
    assert [item["uploaded"] for item in output["state"]] == [False, False, False]


def test_editor_adds_undoes_and_flattens_highlight_and_arrow_annotations():
    script = f"""
const editor = require({json.dumps(str(EDITOR))});
const model = editor.createModel({{width:400,height:300}});
model.setCrop({{x:40,y:30,width:240,height:180}});
model.addHighlight({{x:60,y:50,width:100,height:60}});
model.addArrow({{startX:80,startY:150,endX:220,endY:80}});
const beforeUndo=model.snapshot();
model.undo();
const afterUndo=model.snapshot();
model.addArrow({{startX:80,startY:150,endX:220,endY:80}});
const calls=[];
const context={{
  drawImage:()=>calls.push(['draw']), fillRect:(...args)=>calls.push(['fill',...args]),
  save:()=>calls.push(['save']), restore:()=>calls.push(['restore']),
  beginPath:()=>calls.push(['begin']), moveTo:(...args)=>calls.push(['move',...args]),
  lineTo:(...args)=>calls.push(['line',...args]), stroke:()=>calls.push(['stroke']),
  set fillStyle(value){{calls.push(['fillStyle',value]);}},
  set strokeStyle(value){{calls.push(['strokeStyle',value]);}},
  set lineWidth(value){{calls.push(['lineWidth',value]);}},
  set lineCap(value){{calls.push(['lineCap',value]);}},
  set lineJoin(value){{calls.push(['lineJoin',value]);}},
}};
const canvas={{width:0,height:0,getContext:()=>context}};
editor.paint({{source:{{}},model,canvas}});
process.stdout.write(JSON.stringify({{beforeUndo,afterUndo,calls}}));
"""
    output = run_node(script)
    assert [item["type"] for item in output["beforeUndo"]["annotations"]] == ["highlight", "arrow"]
    assert [item["type"] for item in output["afterUndo"]["annotations"]] == ["highlight"]
    assert ["fillStyle", "rgba(250, 204, 21, 0.38)"] in output["calls"]
    assert ["fill", 20, 20, 100, 60] in output["calls"]
    assert ["strokeStyle", "#facc15"] in output["calls"]
    assert ["move", 40, 120] in output["calls"]
    assert ["line", 180, 50] in output["calls"]
    assert output["calls"].count(["stroke"]) == 1


def test_crop_clips_annotations_to_the_retained_image_area():
    script = f"""
const editor = require({json.dumps(str(EDITOR))});
const model = editor.createModel({{width:300,height:200}});
model.addHighlight({{x:20,y:30,width:100,height:80}});
model.addArrow({{startX:10,startY:90,endX:120,endY:90}});
model.setCrop({{x:50,y:20,width:100,height:120}});
process.stdout.write(JSON.stringify(model.snapshot()));
"""
    output = run_node(script)
    assert output["annotations"] == [
        {"type": "highlight", "x": 50, "y": 30, "width": 70, "height": 80},
        {"type": "arrow", "startX": 50, "startY": 90, "endX": 120, "endY": 90},
    ]


def test_mobile_editor_dialog_is_accessible_touch_sized_and_available_offline():
    html = INDEX.read_text()
    css = CSS.read_text()
    worker = SERVICE_WORKER.read_text()

    assert 'id="edit-create-issue-attachment"' in html
    assert 'id="issue-evidence-editor"' in html
    assert 'role="dialog"' in html
    assert 'aria-labelledby="issue-evidence-editor-heading"' in html
    assert 'id="issue-evidence-editor-canvas"' in html
    assert 'id="highlight-issue-evidence"' in html
    assert 'id="arrow-issue-evidence"' in html
    assert 'aria-label="Screenshot editing canvas. Drag to crop, redact, highlight, or point with an arrow."' in html
    assert 'aria-pressed="true"' in html
    assert "issue-evidence-editor.js" in worker
    assert ".issue-evidence-editor" in css
    assert "min-height:44px" in css
    assert "max-width:100%" in css
    assert "overflow-x:hidden" in css


def test_mobile_conversation_photo_editor_is_available_in_every_composer():
    html = INDEX.read_text()
    dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
    css = CSS.read_text()

    composers = (
        ("issue-attachment", "issueAttachmentController"),
        ("pull-attachment", "pullAttachmentController"),
        ("update-reply-attachment", "updateReplyAttachmentController"),
    )
    for attachment_id, controller in composers:
        assert f'id="edit-{attachment_id}" type="button">Edit photo</button>' in html
        mount = dashboard.split(
            f"const {controller} = issueAttachment.mount({{", 1
        )[1].split("\n  });", 1)[0]
        assert f"edit: qs('#edit-{attachment_id}')" in mount
        assert "dialog: qs('#issue-evidence-editor')" in mount
        assert "appliedMessage: 'Edited photo flattened and ready to send.'" in mount

    assert '<h3 id="issue-evidence-editor-heading">Edit selected photo</h3>' in html
    assert "Edits stay on this device until you send or file." in html
    assert (
        '</form>\n  </section>\n</div>\n'
        '<section class="issue-evidence-editor" id="issue-evidence-editor"'
    ) in html, "the shared editor must not be hidden inside the New issue sheet"
    assert (
        ".conversation-evidence-review .issue-evidence-review-actions "
        "{ grid-template-columns:repeat(4,minmax(0,1fr)); }"
    ) in css
    assert "@media (max-width:390px)" in css
