import json
import subprocess
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
STORE = ROOT / "frontend" / "conversation-reply-draft-store.js"
COORDINATOR = ROOT / "frontend" / "conversation-photo-drafts.js"
DASHBOARD = ROOT / "frontend" / "dashboard.js"
INDEX = ROOT / "frontend" / "index.html"
REGISTRY = ROOT / "frontend" / "private-data-registry.js"
WORKER = ROOT / "frontend" / "service-worker.js"
BUNDLE = ROOT / "src" / "frontend_bundle.py"


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


def test_store_isolates_complete_photo_bundles_by_account_and_conversation():
    script = f"""
const createStore = require({json.dumps(str(STORE))});
const records = new Map();
let login = 'timmy';
const transaction = async (operation, key, value) => {{
  if (operation === 'put') records.set(key, structuredClone(value));
  if (operation === 'get') return records.has(key) ? structuredClone(records.get(key)) : null;
  if (operation === 'delete') records.delete(key);
}};
const store = createStore({{transaction, getOwnerLogin:()=>login}});
const issue = {{kind:'issue', repository:'stackchain/dashboard', number:963}};
const pull = {{kind:'pull', repository:'stackchain/dashboard', number:963}};
const update = {{kind:'update', notificationId:4815}};
const photos = [
  {{filename:'before.webp', contentType:'image/webp', blob:new Blob(['before']), note:'before', operationId:'op-1'}},
  {{filename:'after.webp', contentType:'image/webp', blob:new Blob(['after']), note:'after', operationId:'op-2', confirmed:{{markdown:'![after](saved)'}}}},
];
(async()=>{{
  await store.save(issue, photos);
  await store.save(pull, [photos[1]]);
  await store.save(update, [photos[0]]);
  const restored = await store.load(issue);
  const pullDraft = await store.load(pull);
  const updateDraft = await store.load(update);
  login = 'alexander';
  const wrongAccount = await store.load(issue);
  process.stdout.write(JSON.stringify({{
    restored:await Promise.all(restored.map(async photo=>({{...photo,blobText:await photo.blob.text(),blob:undefined}}))),
    pullCount:pullDraft.length, updateCount:updateDraft.length, wrongAccount, recordCount:records.size,
  }}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = json.loads(run_node(script))

    assert output == {
        "restored": [
            {
                "filename": "before.webp",
                "contentType": "image/webp",
                "note": "before",
                "operationId": "op-1",
                "blobText": "before",
            },
            {
                "filename": "after.webp",
                "contentType": "image/webp",
                "note": "after",
                "operationId": "op-2",
                "confirmed": {"markdown": "![after](saved)"},
                "blobText": "after",
            },
        ],
        "pullCount": 1,
        "updateCount": 1,
        "wrongAccount": None,
        "recordCount": 3,
    }


def test_store_isolates_today_progress_photos_from_conversation_photos():
    script = f"""
const createStore = require({json.dumps(str(STORE))});
const records = new Map();
const transaction = async (operation, key, value) => {{
  if (operation === 'put') records.set(key, structuredClone(value));
  if (operation === 'get') return records.has(key) ? structuredClone(records.get(key)) : null;
  if (operation === 'delete') records.delete(key);
}};
const conversation = createStore({{transaction,getOwnerLogin:()=> 'timmy'}});
const progress = createStore({{transaction,getOwnerLogin:()=> 'timmy',scope:'today-progress'}});
const target={{kind:'issue',repository:'stackchain/dashboard',number:1054}};
(async()=>{{
  await conversation.save(target,[{{filename:'reply.jpg',contentType:'image/jpeg',blob:new Blob(['reply'])}}]);
  await progress.save(target,[{{filename:'progress.jpg',contentType:'image/jpeg',blob:new Blob(['progress'])}}]);
  const reply=await conversation.load(target); const today=await progress.load(target);
  await progress.remove(target);
  process.stdout.write(JSON.stringify({{reply:reply[0].filename,today:today[0].filename,
    replyAfter:(await conversation.load(target))[0].filename,progressAfter:await progress.load(target),count:records.size}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    assert json.loads(run_node(script)) == {
        "reply": "reply.jpg",
        "today": "progress.jpg",
        "replyAfter": "reply.jpg",
        "progressAfter": None,
        "count": 1,
    }


def test_coordinator_ignores_stale_restores_and_clears_only_after_durable_completion():
    script = f"""
const createCoordinator = require({json.dumps(str(COORDINATOR))});
const waits = new Map();
const removed = [];
const restored = [];
let current = [{{filename:'current.webp',contentType:'image/webp',blob:new Blob(['current'])}}];
const store = {{
  load:target=>new Promise(resolve=>waits.set(target.number, resolve)),
  save:async()=>null,
  remove:async target=>removed.push(target.number || target.notificationId),
}};
const controller = {{
  serialize:async()=>current,
  restore:value=>restored.push(value[0].filename),
  clear:()=>{{current=[];}},
}};
const drafts = createCoordinator({{store, lanes:{{issue:{{controller,onError:()=>{{}}}}}}}});
(async()=>{{
  const first = drafts.open('issue', {{kind:'issue',repository:'stackchain/dashboard',number:1}});
  const second = drafts.open('issue', {{kind:'issue',repository:'stackchain/dashboard',number:2}});
  await Promise.resolve();
  waits.get(1)([{{filename:'stale.webp',contentType:'image/webp',blob:new Blob(['stale'])}}]);
  waits.get(2)([{{filename:'fresh.webp',contentType:'image/webp',blob:new Blob(['fresh'])}}]);
  await Promise.all([first, second]);
  await drafts.complete('issue');
  process.stdout.write(JSON.stringify({{restored,removed,hasTarget:drafts.hasTarget('issue')}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = json.loads(run_node(script))
    assert output == {"restored": ["fresh.webp"], "removed": [2], "hasTarget": False}


def test_coordinator_checkpoints_previous_target_before_switching_conversations():
    script = f"""
const createCoordinator = require({json.dumps(str(COORDINATOR))});
const events = [];
let current = [{{filename:'first.webp',contentType:'image/webp',blob:new Blob(['first'])}}];
const store = {{
  load:async target=>target.number === 2 ? [{{filename:'second.webp',contentType:'image/webp',blob:new Blob(['second'])}}] : null,
  save:async (target,attachments)=>events.push('save:' + target.number + ':' + attachments[0].filename),
  remove:async()=>null,
}};
const controller = {{
  serialize:async()=>current,
  restore:value=>{{current=value;events.push('restore:' + value[0].filename);}},
  clear:()=>{{current=[];}},
}};
const drafts = createCoordinator({{store, lanes:{{issue:{{controller}}}}}});
(async()=>{{
  await drafts.open('issue', {{kind:'issue',repository:'stackchain/dashboard',number:1}});
  current=[{{filename:'first.webp',contentType:'image/webp',blob:new Blob(['first'])}}];
  await drafts.switchTo('issue', {{kind:'issue',repository:'stackchain/dashboard',number:2}});
  process.stdout.write(JSON.stringify(events));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    assert json.loads(run_node(script)) == ["save:1:first.webp", "restore:second.webp"]


def test_coordinator_clear_does_not_delete_the_durable_draft_during_reopen():
    script = f"""
const createCoordinator = require({json.dumps(str(COORDINATOR))});
const records=new Map(); let current=[]; let drafts; const restored=[];
const key=target=>target.kind+':'+target.number;
const store={{
  save:async(target,attachments)=>attachments?.length?records.set(key(target),attachments):records.delete(key(target)),
  load:async target=>records.get(key(target))||null,
  remove:async target=>records.delete(key(target)),
}};
const controller={{
  serialize:async()=>current,
  restore:value=>{{current=value;restored.push(value[0].filename);}},
  clear:()=>{{current=[];drafts.checkpoint('today').catch(()=>{{}});}},
}};
drafts=createCoordinator({{store,lanes:{{today:{{controller}}}}}});
const target={{kind:'issue',repository:'stackchain/dashboard',number:1054}};
(async()=>{{
  await drafts.open('today',target);
  current=[{{filename:'proof.webp',contentType:'image/webp',blob:new Blob(['proof'])}}];
  await drafts.checkpoint('today');
  await drafts.switchTo('today',target);
  await Promise.resolve();
  process.stdout.write(JSON.stringify({{restored,saved:(await store.load(target))?.[0]?.filename||null}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    assert json.loads(run_node(script)) == {"restored": ["proof.webp"], "saved": "proof.webp"}


def test_my_work_wires_durable_photo_drafts_into_all_conversation_boundaries():
    dashboard = DASHBOARD.read_text()
    html = INDEX.read_text()
    registry = REGISTRY.read_text()
    worker = WORKER.read_text()
    bundle = BUNDLE.read_text()

    assert '<script src="static/conversation-reply-draft-store.js"></script>' in html
    assert '<script src="static/conversation-photo-drafts.js"></script>' in html
    assert "'stackchain-conversation-reply-drafts-v1'" in registry
    assert "BASE + 'static/conversation-reply-draft-store.js'" in worker
    assert "BASE + 'static/conversation-photo-drafts.js'" in worker
    assert '"static/conversation-reply-draft-store.js"' in bundle
    assert '"static/conversation-photo-drafts.js"' in bundle
    assert "conversationPhotoDrafts.switchTo('issue'," in dashboard
    assert "conversationPhotoDrafts.switchTo('pull'," in dashboard
    assert "conversationPhotoDrafts.switchTo('update'," in dashboard
    assert "conversationPhotoDrafts.leave('issue')" in dashboard
    assert "conversationPhotoDrafts.leave('pull')" in dashboard
    assert "conversationPhotoDrafts.leave('update')" in dashboard
    assert "await conversationPhotoDrafts.complete(kind)" in dashboard
    assert "await conversationPhotoDrafts.complete('issue')" in dashboard
    assert "await conversationPhotoDrafts.complete('pull')" in dashboard
    assert "await conversationPhotoDrafts.complete('update')" in dashboard
