// FILE: _St/IG_StYellow/test_ext_private_views_auto_download.js | ROLE: IG_StYellow 회귀 테스트
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const assert = require('assert');

const root = __dirname;
const side = fs.readFileSync(path.join(root, 'side.js'), 'utf8');
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'));

assert.strictEqual(manifest.version, '2.3.8.3090');
assert.strictEqual(manifest.version_name, '2.3.8.309');
assert.ok(side.includes('const IGBLUE_PACKAGE_VERSION = IGBLUE_BUILD_TAG;'));

// Private/missing views are one explicit state: views=-1 and views_available=0.
for (const token of [
  'function teViewsAvailable(media)',
  'Number(media.views_available) === 0',
  'return Number.isFinite(n) && n >= 0',
  'function teViewCountOrPrivate(media)',
  'if (!teViewsAvailable(media)) return -1',
  'views_available: teViewsAvailable(media) ? 1 : 0'
]) assert.ok(side.includes(token), `missing private-view token: ${token}`);

// Only active Likes/Comments/Reposts take part, and their raw values are summed without weights.
assert.ok(side.includes('{ field: "like_count", min: teMinLikesValue(), label: "Likes" }'));
assert.ok(side.includes('{ field: "comment_count", min: teMinCommentsValue(), label: "Comments" }'));
assert.ok(side.includes('{ field: "media_repost_count", min: teMinRepostsValue(), label: "Reposts" }'));
assert.ok(side.includes('return value > 0 ? sum + value : sum'));
assert.ok(!side.includes('weight: 3, label: "Comments"'));
assert.ok(!side.includes('weight: 4, label: "Reposts"'));

const metricStart = side.indexOf('function teMetricNumber(e)');
const metricEnd = side.indexOf('function teShortNumber(e)', metricStart);
assert.ok(metricStart >= 0 && metricEnd > metricStart, 'metric block missing');
const inputs = {
  top_n_count: {value: '10'},
  igblue_min_comments: {value: '0'},
  igblue_min_likes: {value: '1000'},
  igblue_min_reposts: {value: '0'}
};
const metricCtx = {
  document: {getElementById: (id) => inputs[id] || null},
  teLocalGet: (_key, fallback) => fallback,
  G: ['like_count','comment_count','media_repost_count','created_at','play_count','','engagement_rate','share_count'],
  Z: 4,
  location: {href: 'https://www.instagram.com/dayzhome/'},
  teCurrentTargetUrl: () => 'https://www.instagram.com/dayzhome/'
};
vm.createContext(metricCtx);
vm.runInContext(`${side.slice(metricStart, metricEnd)};
this.viewsAvailable=teViewsAvailable;
this.viewValue=teViewCountOrPrivate;
this.pass=teMediaPassesTopConditions;
this.score=teParticipationScore;
this.sort=teSortCandidateList;`, metricCtx);

const privateA = {code:'A', play_count:-1, views_available:0, like_count:1200, comment_count:999, media_repost_count:999};
const privateB = {code:'B', views_available:0, like_count:1500, comment_count:1, media_repost_count:1};
assert.strictEqual(metricCtx.viewsAvailable(privateA), false);
assert.strictEqual(metricCtx.viewValue(privateA), -1);
assert.strictEqual(metricCtx.pass(privateA, 'play_count', 100000), true);
assert.strictEqual(metricCtx.pass({...privateA, like_count:999}, 'play_count', 100000), false);
assert.strictEqual(metricCtx.score(privateA), 1200, 'only configured Likes must be counted');
const topOne = JSON.parse(JSON.stringify(metricCtx.sort([privateA, privateB], 'play_count'))).slice(0, 1);
assert.deepStrictEqual(topOne.map(x => x.code), ['B'], 'private-view Top 1 must use active-metric plain sum');

// When more engagement conditions are enabled, all are thresholds and their values form the score.
inputs.igblue_min_comments.value = '10';
inputs.igblue_min_reposts.value = '5';
const privateC = {code:'C', play_count:-1, like_count:1000, comment_count:20, media_repost_count:8};
assert.strictEqual(metricCtx.pass(privateC, 'play_count', 100000), true);
assert.strictEqual(metricCtx.score(privateC), 1028);
assert.strictEqual(metricCtx.pass({...privateC, comment_count:9}, 'play_count', 100000), false);

// Main visual No is not required. The immutable auto snapshot becomes the target before AD-03.
const resolveStart = side.indexOf('function teAutoPySnapshotTargetForMedia(media, rankNo)');
const resolveEnd = side.indexOf('async function teAutoPyDownloadMediaPairFromCache', resolveStart);
assert.ok(resolveStart >= 0 && resolveEnd > resolveStart, 'snapshot target resolver block missing');
const resolveCtx = {
  Date,
  teMediaOwnerName: (m) => m.username || '',
  teAutoPyDownloadableKindsFromCache: (m) => [m.video_url ? 'mp4' : 'jpg']
};
vm.createContext(resolveCtx);
vm.runInContext(`${side.slice(resolveStart, resolveEnd)};
this.resolve=teAutoPyResolveTargetForRank;`, resolveCtx);
const snapshotMedia = {
  __igblue_auto_snapshot_no: 1,
  __igblue_auto_snapshot_shortcode: 'PRIVATE001',
  code: 'PRIVATE001', username: 'dayzhome', video_url: 'https://cdn/private001.mp4'
};
const resolved = resolveCtx.resolve(null, snapshotMedia, 1);
assert.strictEqual(resolved.recovered, true);
assert.strictEqual(resolved.source, 'auto_snapshot_rank_target');
assert.strictEqual(resolved.target.rank, 1);
assert.strictEqual(resolved.target.code, 'PRIVATE001');
assert.strictEqual(resolved.target.owner_username, 'dayzhome');

const checkStart = side.indexOf('function teAutoPyTargetReadCheck(target, media, rankNo)');
const checkEnd = side.indexOf('function teAutoPyMediaNorm(mediaType)', checkStart);
assert.ok(checkStart >= 0 && checkEnd > checkStart, 'AD-03 target read block missing');
const checkCtx = {
  Date,
  teAutoPySourceUrlForMedia: (m, _meta, kind) => kind === 'mp4' ? (m.video_url || '') : (m.img_origin || ''),
  teMediaOwnerName: (m) => m.username || '',
  teAutoPyStopReasonKo: (code, detail) => `${code}:${detail}`
};
vm.createContext(checkCtx);
vm.runInContext(`${side.slice(checkStart, checkEnd)}; this.check=teAutoPyTargetReadCheck;`, checkCtx);
const checked = checkCtx.check(resolved.target, snapshotMedia, 1);
assert.strictEqual(checked.ok, true, 'snapshot No001 must satisfy AD-03 without Main visual rank DOM');
assert.strictEqual(checked.owner_username, 'dayzhome');
assert.strictEqual(checked.shortcode, 'PRIVATE001');
assert.strictEqual(checked.asset_kind, 'video');

// v278: Main active 정보가 순간적으로 비어도 정확한 snapshot이면 기존 직접 다운로드 경로를 복구한다.
assert.ok(!side.includes('allow_cache_fallback: false'));
assert.ok(side.includes('S00_SNAPSHOT_START_CURRENT'));
assert.ok(side.includes('set_internal_current_no_without_download'));
const ad03Trace = side.indexOf('teAutoPyFeatureCheck("AD-03-E01", "[예외추적]"');
const ad03Recovered = side.indexOf('teAutoPyFeatureCheck("AD-03", "[복구완료]"', ad03Trace);
const ad03Fail = side.indexOf('teAutoPyFeatureCheck("AD-03", "[체크실패]"', ad03Recovered);
assert.ok(ad03Trace >= 0 && ad03Recovered > ad03Trace && ad03Fail > ad03Recovered, 'AD-03 recovery/fail order missing');
const recoveryBlock = side.slice(ad03Trace, ad03Fail);
assert.ok(recoveryBlock.includes('teAutoPyTryCacheDownloadForRank'));
assert.ok(recoveryBlock.includes('snapshot_cache_recovery_complete'));
assert.ok(recoveryBlock.includes('S04_CACHE_RECOVERY_ADVANCE'));
assert.ok(side.includes('actual_conflict'));

const conflictResolved = resolveCtx.resolve({rank: 1, code: 'OTHER999'}, snapshotMedia, 1);
assert.strictEqual(conflictResolved.recovered, false, 'actual Main shortcode conflict must not be hidden by snapshot');
assert.strictEqual(conflictResolved.conflict, true);
assert.strictEqual(conflictResolved.source, 'main_active_conflict');

console.log('OK test_ext_private_views_auto_download v278');
