/**
 * Arcrun#106 迴歸守衛 —— 「更新完，設定頁還看得到版本號，而且是**這次**的版本號」
 *
 * 2026-08-12 實害：leo 更新完 leo21c，Portal 設定頁的版本欄變成
 * 「無法讀取目前版本（知識庫服務可能正在啟動）」。
 * 根因：`ARCRUN_BUNDLE_VERSION` 是部署時注入的 plain_text var，**只有安裝器會注入**；
 * CLI 這條路重部署時 wrangler 整份覆蓋 toml，沒寫的 var 直接消失 ⇒ 標籤被洗掉。
 * #97 修好了「櫃子」（KV/D1/Vectorize 沿用既有），**沒修「櫃子上的標籤」**。
 *
 * 這份測試守兩件相反的事（本次的核心判斷）：
 *   · 設定類 var（安裝器注入的 PORTAL_MAIL_RELAY_BASE 之類）＝使用者實例的事實 → **沿用**
 *   · 版本標籤 ARCRUN_BUNDLE_VERSION ＝這份成品的屬性 → **每趟重烙，絕不沿用舊值**
 *     （沿用舊值 = 一個永遠停在安裝當天的假標籤，比沒有標籤更糟）
 *
 * 全部離線跑：真的 wrangler.toml + 真的 render/inject 程式碼，fetch 用假的，不碰任何實例。
 */

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import {
  renderWranglerToml,
  preservedVars,
  applyVars,
  resolveBundleStamp,
  CLI_MANAGED_VARS,
  VERSION_STAMP_WORKER,
  type DeployContext,
} from '../src/lib/deploy.ts';
import { planResources, type ResourceApi, type ScriptBindings } from '../src/lib/resource-resolver.ts';

const REPO = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..');
const CYPHER_TOML = readFileSync(join(REPO, 'cypher-executor', 'wrangler.toml'), 'utf8');

const CTX: DeployContext = {
  accountId: 'acc-user-123',
  apiToken: 'token',
  workerSubdomain: 'user-sub',
  selfHosted: true,
  kbdbEmbed: true,
};

/** 一台「安裝器裝出來、已經跑過的」實例上，cypher worker 現在掛著的 plain_text var。 */
const LIVE_VARS: Record<string, string> = {
  ARCRUN_BUNDLE_VERSION: '1.4.29',                                  // 安裝當時的舊標籤
  PORTAL_MAIL_RELAY_BASE: 'https://mail.example.com',               // 安裝器注入、repo toml 沒有 → 洗掉就寄不出信
  CONSOLE_TENANT: 'someone-else',                                   // repo toml 寫死 "leo"，不能拿官方值蓋掉人家的
  WORKER_SUBDOMAIN: 'user-sub',                                     // CLI 自己算
  CF_ACCOUNT_ID: 'acc-user-123',                                    // CLI 自己算
  MULTI_TENANT: 'false',                                            // CLI 自己算
  ENVIRONMENT: 'production',                                        // 與 toml 同值 → 不必重寫
};

/** 從 render 過的 toml 讀 [vars] 區塊（只看未註解的行）。 */
function readVars(toml: string): Record<string, string> {
  const out: Record<string, string> = {};
  let inVars = false;
  for (const raw of toml.split('\n')) {
    const line = raw.trim();
    if (/^\[\[?[A-Za-z0-9_]+\]?\]$/.test(line)) { inVars = line === '[vars]'; continue; }
    if (!inVars || line.startsWith('#')) continue;
    const m = line.match(/^([A-Za-z0-9_]+)\s*=\s*"([^"]*)"/);
    if (m) out[m[1]] = m[2];
  }
  return out;
}

// ═════════════════════════════════════════════════════════════════════════════
// ① 病灶本身：舊行為會把標籤洗掉
// ═════════════════════════════════════════════════════════════════════════════

test('#106 ①：repo 的 cypher toml 本來就沒有 ARCRUN_BUNDLE_VERSION——不補就是洗掉（病灶重現）', () => {
  const rendered = renderWranglerToml(CYPHER_TOML, CTX, new Map());
  assert.equal(
    readVars(rendered).ARCRUN_BUNDLE_VERSION,
    undefined,
    '若這行開始有值，表示 toml 自己帶了版本標籤，本測試的前提要重寫',
  );
});

// ═════════════════════════════════════════════════════════════════════════════
// ② 設定類 var：沿用實例上的事實
// ═════════════════════════════════════════════════════════════════════════════

test('#106 ②：安裝器注入、repo toml 沒有的 var 會被沿用（不再被重部署洗掉）', () => {
  const keep = preservedVars(LIVE_VARS, CYPHER_TOML);
  assert.equal(keep.PORTAL_MAIL_RELAY_BASE, 'https://mail.example.com');
  // repo toml 寫死的是官方值，使用者實例上的值才是事實
  assert.equal(keep.CONSOLE_TENANT, 'someone-else');
  // 與 toml 同值 → 不需要重寫進去（雜訊）
  assert.equal(keep.ENVIRONMENT, undefined);
});

test('#106 ③：CLI 自己算的 var 一律不沿用（沿用等於拿舊值蓋掉這趟的正解）', () => {
  const keep = preservedVars({ ...LIVE_VARS, WORKER_SUBDOMAIN: 'OLD-sub', CF_ACCOUNT_ID: 'OLD-acc' }, CYPHER_TOML);
  for (const managed of CLI_MANAGED_VARS) {
    assert.equal(keep[managed], undefined, `${managed} 不該被沿用`);
  }
  // 而且注入完的 toml 裡，這些值仍是這趟算出來的那個
  const rendered = renderWranglerToml(CYPHER_TOML, CTX, new Map(), keep);
  const vars = readVars(rendered);
  assert.equal(vars.WORKER_SUBDOMAIN, 'user-sub');
  assert.equal(vars.CF_ACCOUNT_ID, 'acc-user-123');
  assert.equal(vars.MULTI_TENANT, 'false');
  assert.equal(vars.KBDB_BASE_URL, 'https://arcrun-kbdb.user-sub.workers.dev');
});

// ═════════════════════════════════════════════════════════════════════════════
// ③ 版本標籤：重烙，不沿用
// ═════════════════════════════════════════════════════════════════════════════

test('#106 ④：版本標籤取「發行頻道公告的 release」＋ 實際 commit，不是沿用舊值', async () => {
  const fakeFetch = (async () =>
    new Response(JSON.stringify({ release: '1.4.41', pin: 'ba81439' }), { status: 200 })) as typeof fetch;
  const stamp = await resolveBundleStamp('main', 'f87d0e92f49690253e7c89c5badc82a08eb5d21b', fakeFetch);
  assert.equal(stamp.version, '1.4.41');
  assert.notEqual(stamp.version, LIVE_VARS.ARCRUN_BUNDLE_VERSION);   // ← 這就是本 issue
  assert.equal(stamp.commit, 'f87d0e92f49690253e7c89c5badc82a08eb5d21b');
  assert.match(stamp.version, /^\d+\.\d+\.\d+$/, 'Portal 拿它跟 /api/latest 比 semver，必須是純 semver');
});

test('#106 ⑤：查不到發行版號時誠實標成 commit 版，**不**沿用舊值、也不掰一個 semver', async () => {
  const fakeFetch = (async () => { throw new Error('offline'); }) as typeof fetch;
  const stamp = await resolveBundleStamp('main', 'f87d0e92f49690253e7c89c5badc82a08eb5d21b', fakeFetch);
  assert.match(stamp.version, /^\d{4}-\d{2}-\d{2}\+f87d0e9$/);
  assert.notEqual(stamp.version, LIVE_VARS.ARCRUN_BUNDLE_VERSION);
  assert.doesNotMatch(stamp.version, /^\d+\.\d+\.\d+$/, '掰一個 semver 會讓 Portal 假裝「已是最新版」');
});

test('#106 ⑥：發行頻道回了不是 semver 的東西 → 當成查不到（不把垃圾當版號烙上去）', async () => {
  const fakeFetch = (async () =>
    new Response(JSON.stringify({ release: 'latest' }), { status: 200 })) as typeof fetch;
  const stamp = await resolveBundleStamp('main', 'abc1234def', fakeFetch);
  assert.match(stamp.version, /^\d{4}-\d{2}-\d{2}\+abc1234$/);
});

// ═════════════════════════════════════════════════════════════════════════════
// ④ 端到端（離線）：一台已安裝的實例跑一次更新，Portal 讀得到的那個欄位長什麼樣
// ═════════════════════════════════════════════════════════════════════════════

test('#106 ⑦：模擬更新——版本標籤變新、設定 var 一個不少、資源沿用不受影響', async () => {
  const api: ResourceApi = {
    async getScriptBindings(script: string): Promise<ScriptBindings> {
      if (script !== VERSION_STAMP_WORKER) return { deployed: false, bindings: [], vars: {} };
      return {
        deployed: true,
        bindings: [
          { kind: 'kv_namespace', binding: 'WEBHOOKS', value: 'kv-webhooks' },
          { kind: 'kv_namespace', binding: 'CREDENTIALS_KV', value: 'kv-creds' },
          { kind: 'kv_namespace', binding: 'RECIPES', value: 'kv-recipes' },
          { kind: 'kv_namespace', binding: 'USERS_KV', value: 'kv-users' },
          { kind: 'kv_namespace', binding: 'SESSIONS_KV', value: 'kv-sessions' },
          { kind: 'kv_namespace', binding: 'ANALYTICS_KV', value: 'kv-analytics' },
          { kind: 'kv_namespace', binding: 'EXEC_CONTEXT', value: 'kv-exec' },
          { kind: 'd1', binding: 'CREDENTIALS_DB', value: 'd1-kbdb' },
        ],
        vars: LIVE_VARS,
      };
    },
    async listKvNamespaces() {
      return new Map([
        ['a', 'kv-webhooks'], ['b', 'kv-creds'], ['c', 'kv-recipes'], ['d', 'kv-users'],
        ['e', 'kv-sessions'], ['f', 'kv-analytics'], ['g', 'kv-exec'],
      ]);
    },
    async listD1Databases() { return new Map([['arcrun-kbdb', 'd1-kbdb']]); },
    async listVectorizeIndexes() { return []; },
    async createKvNamespace() { throw new Error('這趟不該新建任何 KV'); },
    async createD1Database() { throw new Error('這趟不該新建 D1'); },
    async createVectorizeIndex() { throw new Error('這趟不該新建 Vectorize'); },
  };

  const preview = renderWranglerToml(CYPHER_TOML, CTX, new Map());
  const { parseWranglerRequirements } = await import('../src/lib/resource-resolver.ts');
  const parsed = parseWranglerRequirements(preview);
  const plan = await planResources(
    api,
    parsed.bindings.map((b) => ({ ...b, worker: parsed.script })),
    'update',
  );
  assert.deepEqual(plan.blockers, []);
  // 讀綁定時順手把 var 帶回來——不另外打一次 API
  assert.equal(plan.liveVars.get(VERSION_STAMP_WORKER)?.PORTAL_MAIL_RELAY_BASE, 'https://mail.example.com');

  const fakeFetch = (async () =>
    new Response(JSON.stringify({ release: '1.4.41' }), { status: 200 })) as typeof fetch;
  const stamp = await resolveBundleStamp('main', 'f87d0e92f49690253e7c89c5badc82a08eb5d21b', fakeFetch);
  const extra = {
    ...preservedVars(plan.liveVars.get(parsed.script), CYPHER_TOML),
    ARCRUN_BUNDLE_VERSION: stamp.version,
    ARCRUN_BUNDLE_COMMIT: stamp.commit!,
  };

  const deployed = readVars(renderWranglerToml(CYPHER_TOML, CTX, new Map(), extra));

  // ① Portal 設定頁讀的就是這個欄位——更新完必須有值，且是**這趟**的版本
  assert.equal(deployed.ARCRUN_BUNDLE_VERSION, '1.4.41');
  assert.equal(deployed.ARCRUN_BUNDLE_COMMIT, 'f87d0e92f49690253e7c89c5badc82a08eb5d21b');
  // ② 安裝器注入的設定沒有在更新中消失
  assert.equal(deployed.PORTAL_MAIL_RELAY_BASE, 'https://mail.example.com');
  assert.equal(deployed.CONSOLE_TENANT, 'someone-else');
  // ③ CLI 自己算的仍然是這趟算出來的
  assert.equal(deployed.WORKER_SUBDOMAIN, 'user-sub');
  assert.equal(deployed.MULTI_TENANT, 'false');
});

// ═════════════════════════════════════════════════════════════════════════════
// ⑤ applyVars 的三種既有狀態 + 不弄壞別的區塊
// ═════════════════════════════════════════════════════════════════════════════

test('#106 ⑧：applyVars——改既有行／取消註解／插進 [vars]／連 [vars] 都沒有時新開一段', () => {
  assert.match(applyVars('[vars]\nA = "old"\n', { A: 'new' }), /^\[vars\]\nA = "new"\n$/);
  assert.match(applyVars('[vars]\n# A = "old"\n', { A: 'new' }), /A = "new"/);
  assert.match(applyVars('[vars]\nB = "b"\n', { A: 'a' }), /\[vars\]\nA = "a"\nB = "b"/);
  const noVars = applyVars('name = "w"\n', { A: 'a' });
  assert.match(noVars, /\[vars\]\nA = "a"/);
  assert.match(noVars, /^name = "w"/);
});

test('#106 ⑨：var 值裡的引號／反斜線會被轉義（不會產生壞掉的 toml）', () => {
  const out = applyVars('[vars]\n', { A: 'say "hi"\\path' });
  assert.match(out, /A = "say \\"hi\\"\\\\path"/);
});

test('#106 ⑨b：值裡有 $& / $1 也照原樣寫出（replace 反向參照陷阱）', () => {
  assert.match(applyVars('[vars]\nA = "old"\n', { A: 'x$&y$1z' }), /A = "x\$&y\$1z"/);
  assert.match(applyVars('[vars]\n', { A: 'x$&y' }), /A = "x\$&y"/);
  // 怪名字不寫進去（不拿它組正規式）
  assert.equal(applyVars('[vars]\n', { 'BAD NAME': 'v' }), '[vars]\n');
});

test('#106 ⑩：注入 var 不影響資源綁定解析（預覽與實際寫入看到的是同一份需求）', async () => {
  const { parseWranglerRequirements } = await import('../src/lib/resource-resolver.ts');
  const withoutVars = parseWranglerRequirements(renderWranglerToml(CYPHER_TOML, CTX, new Map()));
  const withVars = parseWranglerRequirements(
    renderWranglerToml(CYPHER_TOML, CTX, new Map(), { ARCRUN_BUNDLE_VERSION: '1.4.41', X: 'y' }),
  );
  assert.equal(withVars.script, withoutVars.script);
  assert.deepEqual(withVars.bindings, withoutVars.bindings);
});
