const fs = require('fs');
const path = require('path');
const assert = require('assert');
const vm = require('vm');
const root = __dirname;
const read = (name) => fs.readFileSync(path.join(root, name), 'utf8');
const manifest = JSON.parse(read('manifest.json'));
assert.strictEqual(manifest.version, '2.3.8.228');
assert.strictEqual(manifest.version_name, '2.3.8.228');
const refs = [];
for (const item of manifest.content_scripts || []) refs.push(...(item.js || []));
for (const item of manifest.web_accessible_resources || []) refs.push(...(item.resources || []).filter(x => !x.includes('*')));
for (const ref of refs) assert.ok(fs.existsSync(path.join(root, ref)), `missing manifest resource: ${ref}`);
assert.ok(!refs.includes('js/ig_origin_calibration.js'));
const bg = read('background.js');
const side = read('side.js');
assert.ok(side.includes('const IGBLUE_PACKAGE_VERSION = "v228";'));
const content = read('content_instagram.js');

const inject = read('js/instagram_inject.js');
assert.ok(inject.includes('return t?[i,n,r].filter(Boolean).join(\"\\n\"):[\"IG\",n].filter(Boolean).join(\" \")'), 'MAIN overlay must show IG + account when rank label is absent');
const compactFnMatch = inject.match(/function teMainCompactLabel\(e\)\{[^}]+\}/);
assert.ok(compactFnMatch, 'teMainCompactLabel function missing');
const compactCtx = {
  teMainRankLabelMap: new Map(),
  teMainOwnerText: (media, rank) => String((rank && rank.username) || (media && media.username) || '').replace(/^@/, ''),
  teMainRankNoText: (no) => Number(no) > 0 ? `No ${String(Number(no)).padStart(3, '0')}` : '',
  teMainKindText: () => 'VID'
};
vm.createContext(compactCtx);
vm.runInContext(`${compactFnMatch[0]}; this.teMainCompactLabel = teMainCompactLabel;`, compactCtx);
assert.strictEqual(compactCtx.teMainCompactLabel({ code: 'abc', username: 'double_king0.1' }), 'IG double_king0.1');
assert.strictEqual(compactCtx.teMainCompactLabel({ code: 'abc', username: '' }), 'IG');
compactCtx.teMainRankLabelMap.set('abc', { no: 3, username: 'double_king0.1', kind: 'VID' });
assert.strictEqual(compactCtx.teMainCompactLabel({ code: 'abc', username: 'double_king0.1' }), 'No 003\ndouble_king0.1\nVID');

assert.ok(side.includes('teApplySideAccountLabel(c[0], n)'), 'Side account label must be independent');
assert.ok(side.includes('teApplySideRankLabel(t.querySelector(".igblue-side-rank")'), 'Side No rank label must use a separate row');
assert.ok(side.includes('span.textContent = account ? `IG ${account}` : "IG"'), 'Side account format must be IG account');
assert.ok(side.includes('IGBLUE_ACCOUNT_DELETED_PREFIX = "🗑️"'), 'deleted account emoji marker missing');
assert.ok(side.includes('teAutoPySyncDeletedAccountsFromAnalyze8780'), 'Analyze8780 deleted account sync missing');
assert.ok(side.includes('filter((entry) => !entry.deleted)'), 'deleted accounts must be excluded from queue counting');

const sideHtml = read('side.html');
assert.ok(sideHtml.includes('id="igblue_account_url_save"'), 'explicit account list save button missing');
assert.ok(sideHtml.includes('>💾 저장</button>'), 'save button label missing');
assert.ok(side.includes('let teAccountQueueDirty = false'), 'account list dirty state missing');
assert.ok(side.includes('saveBtn && saveBtn.addEventListener("click"'), 'explicit save click handler missing');
assert.ok(side.includes('teAutoPySetAccountQueueDirty(true)'), 'unsaved account list state missing');
assert.ok(!side.includes('const saveFromBlur ='), 'legacy blur auto-save must be removed');
assert.ok(!side.includes('el.addEventListener("blur", saveFromBlur)'), 'blur auto-save listener remains');
assert.ok(side.includes('if (teAccountQueueDirty) return false'), 'deleted-account polling must not overwrite unsaved edits');

const sideWs = read('side_ws.js');
for (const token of ['IGBLUE_DOWNLOAD_URL', 'chrome.downloads.download', 'ig_media_exists', 'ig_media_downloaded']) assert.ok(bg.includes(token), `missing direct download token: ${token}`);
for (const token of ['teAutoPyDirectDownloadAsset', 'teAutoPyDownloadMediaPairDirect', 'teAutoPyStartOnceFromUi']) assert.ok(side.includes(token), `missing direct auto token: ${token}`);
for (const token of ['IGBLUE_AUTO_OWNER_CLAIM', 'IGBLUE_RED_OWNER_UPDATED', 'teAutoPyWaitUserMouseIdle', 'teAutoPyRtcClickMedia', 'teAutoPyOriginFindBeforeStart', 'igblue_get_hover_target']) assert.ok(!side.includes(token), `retired side token remains: ${token}`);
for (const token of ['igblue_hover_target', 'igblue_browser_origin', 'igblue_mouse_sync', 'igblue_run_origin_find', 'igblue_get_last_dom_click_ack']) assert.ok(!content.includes(token), `retired content token remains: ${token}`);
assert.ok(content.includes('igblue_auto_py_next'));
assert.ok(content.includes('igblue_get_active_download_target'));
assert.ok(!sideWs.includes('user_mouse'));
assert.ok(!fs.existsSync(path.join(root, 'js/ig_origin_calibration.js')));
for (const token of ["BULK_ALL", "BULK_FILTERED", "TOP_SORTED", "MANUAL_SINGLE", "client_batch_key", "client_event_id"]) assert.ok(side.includes(token) || content.includes(token), `missing MEDIA_SAVED flow token: ${token}`);

assert.ok(side.includes('const normalizedFlowMeta = tePrepareDownloadFlowMeta'), 'manual carousel shared batch normalization missing');
assert.ok(side.includes('const flowEntryKey = `${normalizedFlowMeta.client_batch_key}'), 'per-file event key must use actual batch key');
assert.ok(!side.includes('(flowMeta && flowMeta.client_batch_key) || "batch"'), 'literal fallback batch key remains');
assert.ok(bg.includes('donePayload.status="partial"'), 'WS notify exception must be partial');
assert.ok(content.includes('파일 저장 완료\\nDB 처리 실패'), 'MAIN overlay partial failure status missing');
console.log('OK test_ext_module direct-download-only');
