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.237');
assert.strictEqual(manifest.version_name, '2.3.8.237');
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 = "v237";'));
const content = read('content_instagram.js');

assert.ok(!side.includes('window.close()'), 'Side must never close itself after opening a popup');
assert.ok(!bg.includes('chrome.sidePanel.close'), 'background must not close Side Panel');
assert.ok(!bg.includes('chrome.tabs.remove'), 'background must not close Instagram tabs');
assert.ok(!bg.includes('chrome.windows.remove'), 'background must not close Chrome windows');
assert.ok(!side.includes('teArmWsAutoEnter("auto_direct_download"'), 'auto download must not depend on Save As Enter');
assert.ok(!side.includes('teArmWsAutoEnter("main_manual_download_button"'), 'manual download must not depend on Save As Enter');
assert.ok(!bg.includes('api/cdp/download-root/ensure'), 'main download must not depend on Auto8700 CDP root');
assert.ok(bg.includes('function igblueOriginalDownloadFileName'), 'original Instagram filename resolver missing');
assert.ok(bg.includes('const normalizedPrefix = igblueNormalizeDownloadPrefix(msg, ext);'), 'direct download must validate the full message/meta object');
assert.ok(!bg.includes('igblueNormalizeDownloadPrefix(msg.prefix, ext)'), 'v233 string-only prefix regression remains');
assert.ok(bg.includes('const relativeFile = accountFolderMerge ? originalFileName : `${igblueSafePath(ownerAccount, "instagram")}/${originalFileName}`'), 'physical path must be account/original filename');
assert.ok(bg.includes('"ig_media_download_direct"'), 'WS direct download action missing');
assert.ok(bg.includes('function igbluePollWsQueuedJob'), 'WS queued result polling missing');
assert.ok(!bg.includes('chrome.downloads.download({'), 'Chrome download API must not open Save As');
assert.ok(bg.includes('finalStatus = String(result.status || "ok") === "partial" ? "partial" : "ok"'), 'file-saved DB failure must remain partial');
assert.ok(bg.includes('const finalize = result.finalize && typeof result.finalize === "object" ? result.finalize : {}'), 'WS direct finalize result must be flattened');
assert.ok(bg.includes('notify_aiwk:finalize'), 'manual/auto shared finalize parser must receive WS finalize data');

assert.ok(manifest.description.includes('automatic No-ranked direct downloads'), 'manifest description must describe v237 automatic download repair');
assert.ok(bg.includes('const precheckOptions = msg && msg.auto_direct_download ? {priority:true, force:true, force_probe:true} : {}'), 'automatic background precheck must force the WS connection probe');
assert.ok(bg.includes('if(options.force_probe === true && options.force === true)') && bg.includes('igblueWsBackoffUntil = 0'), 'forced automatic probe must bypass stale WS backoff');
assert.ok(bg.includes('igblueLocalWsRequest("ig_media_exists"') && bg.includes('8000, precheckOptions'), 'automatic duplicate precheck must use forced options');
for (const id of ['AD-01', 'AD-02', 'AD-03', 'AD-04', 'AD-05', 'AD-06', 'AD-07']) assert.ok(side.includes(`"${id}"`) || bg.includes(`"${id}"`), `missing automatic feature check: ${id}`);
for (const state of ['[체크시작]', '[체크완료]', '[체크실패]']) assert.ok(side.includes(state) || bg.includes(state), `missing Korean check state: ${state}`);
assert.ok(side.includes('const teAutoPyFeatureCheckSeen = new Set()'), 'feature-check dedupe set missing');
assert.ok(side.includes('feature_check_mode: true'), 'legacy detailed cycle logging must be marked for concise WS handling');
assert.ok(side.includes('if (String(e.status || "") === "feature_check") return;'), 'feature-check messages must not pollute the download progress UI');

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(inject.includes('installIgblueMainIdentitySplitV235'), 'MAIN identity split regression guard missing');
assert.ok(inject.includes('igblue-main-account-row'), 'MAIN IG account row missing');
assert.ok(inject.includes('igblue-main-rank-row'), 'MAIN No row missing');
assert.ok(inject.includes('igblue-main-kind-row'), 'MAIN media kind row missing');

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 && entry.enabled !== false)'), '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 (!el || teAccountQueueDirty) return false'), 'deleted-account polling must not overwrite unsaved edits');
assert.ok(sideHtml.includes('>수집계정 목록</span>'), 'collection account username heading missing');
assert.ok(sideHtml.includes('>표시계정추가</button>'), 'visible IG account add button missing');
assert.ok(side.includes('function teAutoPyNormalizeAccountUsername(raw)'), 'username-only queue normalizer missing');
assert.ok(side.includes('teOwnerAccountForMedia ? teOwnerAccountForMedia(media)'), 'visible account collection must use injected media owner username');
assert.ok(side.includes('http://127.0.0.1:8780/api/collection-accounts'), 'Analyze8780 collection account API missing');
assert.ok(side.includes('method: "PUT"'), 'collection account SQLite save must use PUT');
assert.ok(side.includes('return username ? `https://www.instagram.com/${username}/` : ""'), 'profile URL must be generated only at navigation time');
assert.ok(!sideHtml.includes('계정을URL로변환'), 'legacy URL conversion button remains');
assert.ok(side.includes('..\\\\_StDown\\\\instagram\\\\${n}\\\\'), 'download folder UI must show fixed _StDown/instagram/account path');

const sideWs = read('side_ws.js');
for (const token of ['IGBLUE_DOWNLOAD_URL', 'ig_media_exists', 'ig_media_download_direct', 'ig_thread_queue_result']) 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');
const parserStart = side.indexOf('function teAutoPyDirectReportResultFromResponse(res)');
const parserEnd = side.indexOf('async function teAutoPyDirectDownloadAsset', parserStart);
assert.ok(parserStart >= 0 && parserEnd > parserStart, 'direct finalize parser block missing');
const parserCtx = {};
vm.createContext(parserCtx);
vm.runInContext(`${side.slice(parserStart, parserEnd)}; this.parseDirect = teAutoPyDirectReportResultFromResponse;`, parserCtx);
const parsedDirect = parserCtx.parseDirect({
  status: 'ok', file_path: 'D:/_StDown/instagram/account/original.mp4', file_saved: true, storage_ok: true,
  result: { finalize_ok: true, file_path: 'D:/_StDown/instagram/account/original.mp4', finalize: { ok: true, storage_ok: true, json_path: 'D:/_StDown/instagram/account.json', report_ok: true, report_path: 'D:/_StDown/instagram/account.html' } }
});
assert.strictEqual(parsedDirect.ok, true, 'automatic WS direct response must be accepted as completed');
assert.strictEqual(parsedDirect.file_saved, true);
assert.strictEqual(parsedDirect.json_saved, true);
assert.strictEqual(parsedDirect.report_saved, true);

assert.ok(content.includes('파일 저장 완료\\nDB 처리 실패'), 'MAIN overlay partial failure status missing');
console.log('OK test_ext_module direct-download-only');
