/**
 * Arcrun#108 迴歸守衛 —— 「雲端讀資料用的命名空間，要跟你寫資料用的那個一致」
 *
 * 2026-08-12 實害：leo 的藏書地圖回 0 個庫，實際有 1854 條三元組。
 * 根因：你 push 工作流、小幫手上傳知識、MCP 查詢都用 `~/.arcrun/config.yaml` 的 `api_key`
 * （leo ＝ `bfezv28v`），但 cypher 讀取時的 owner_id 來自 worker 環境變數
 * ——而那個變數是 repo toml 帶的**官方 prod 值** `CONSOLE_TENANT = "leo"`。
 * 寫在 A、讀在 B，全被過濾掉。
 *
 * 這份測試守兩件相反的事（本次的核心判斷）：
 *   · 驗得到知識 → **寫** `ARCRUN_NAMESPACE`，讓讀寫兩端對齊
 *   · 驗不到 / 問不到 → **一個字都不動**，既有值原封保留
 *     （無條件覆蓋會把一台「知識本來就寫在 CONSOLE_TENANT 底下」的一鍵安裝實例指向空的那一格
 *      ——那就是 #97/#106 那類「更新一次把人家的東西弄不見」，比原本的 bug 更糟）
 *
 * 全部離線跑：真的 wrangler.toml + 真的 render 程式碼，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,
  namespaceHasKnowledge,
  VERSION_STAMP_WORKER,
  type DeployContext,
} from '../src/lib/deploy.ts';

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

/** leo 的真實命名空間（2026-08-11 回灌時定名，見 Leo/mira#8）。 */
const LEO_NS = 'bfezv28v';

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

function readVars(toml: string): Record<string, string> {
  const out: Record<string, string> = {};
  let inVars = false;
  for (const line of toml.split('\n')) {
    if (/^\s*\[vars\]/.test(line)) { inVars = true; continue; }
    if (/^\s*\[/.test(line)) { inVars = false; continue; }
    if (!inVars) continue;
    const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*"([^"]*)"/);
    if (m) out[m[1]] = m[2];
  }
  return out;
}

/** 模擬 downloadAndDeploy 那段：沿用既有 var，再疊上這趟 CLI 算出來的值。 */
function deployedVars(ctx: DeployContext, live: Record<string, string>): Record<string, string> {
  const keep = preservedVars(live, CYPHER_TOML);
  const extra: Record<string, string> = { ...keep };
  if (ctx.knowledgeNamespace) extra.ARCRUN_NAMESPACE = ctx.knowledgeNamespace;
  return readVars(renderWranglerToml(CYPHER_TOML, ctx, new Map(), extra));
}

// ── ① 驗得到知識 → 寫進去 ────────────────────────────────────────────────────

test('#108 給了 knowledgeNamespace → cypher [vars] 出現 ARCRUN_NAMESPACE（讀寫兩端終於同一個值）', () => {
  const vars = deployedVars({ ...CTX, knowledgeNamespace: LEO_NS }, {});
  assert.equal(vars.ARCRUN_NAMESPACE, LEO_NS);
  // CONSOLE_TENANT 一個字都不能動——它同時是帳號子 namespace 的組成，改了舊實例登不進去
  assert.equal(vars.CONSOLE_TENANT, 'leo');
});

test('#108 蓋得過 worker 上的舊值（改名／搬遷後 acr update 要能修正，不是永遠沿用第一次那個）', () => {
  const vars = deployedVars({ ...CTX, knowledgeNamespace: LEO_NS }, { ARCRUN_NAMESPACE: 'stale-ns' });
  assert.equal(vars.ARCRUN_NAMESPACE, LEO_NS);
});

// ── ② 驗不到 → 什麼都不動（比 bug 更糟的是把人家原本正常的實例弄空）──────────────

test('#108 沒給 knowledgeNamespace → 既有的 ARCRUN_NAMESPACE 原封保留（不因為這趟驗不到就洗掉）', () => {
  const vars = deployedVars(CTX, { ARCRUN_NAMESPACE: 'user-existing-ns' });
  assert.equal(vars.ARCRUN_NAMESPACE, 'user-existing-ns');
});

test('#108 沒給、worker 上也沒有 → 不注入（回退 CONSOLE_TENANT，舊實例行為一字不變）', () => {
  const vars = deployedVars(CTX, {});
  assert.equal(vars.ARCRUN_NAMESPACE, undefined);
  assert.equal(vars.CONSOLE_TENANT, 'leo');
});

test('#108 ARCRUN_NAMESPACE 不在 CLI_MANAGED_VARS：它「不是每趟重算」而是「驗到才寫」，' +
  '列進去會讓驗不到的那趟把既有值一起洗掉', async () => {
  const { CLI_MANAGED_VARS } = await import('../src/lib/deploy.ts');
  assert.equal((CLI_MANAGED_VARS as readonly string[]).includes('ARCRUN_NAMESPACE'), false);
});

test('#108 只烙在 cypher 這顆 worker（其他 worker 不需要知識命名空間）', () => {
  assert.equal(VERSION_STAMP_WORKER, 'arcrun-cypher-executor');
});

// ── ③ 「先驗再寫」那支探針的三態 ───────────────────────────────────────────────

test('namespaceHasKnowledge：這個命名空間底下查得到庫 → true（可以安全寫進去）', async () => {
  const calls: string[] = [];
  const orig = globalThis.fetch;
  globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
    calls.push(String(url));
    assert.equal((init?.headers as Record<string, string>)['X-Arcrun-API-Key'], LEO_NS);
    return new Response(JSON.stringify({ success: true, libraries: [{ library: 'kb' }], count: 1 }), { status: 200 });
  }) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', LEO_NS), true);
    assert.equal(calls[0], `https://cypher.example.dev/kbdb/map?owner_id=${LEO_NS}`);
  } finally {
    globalThis.fetch = orig;
  }
});

test('namespaceHasKnowledge：查得到但是空的 → false（知識可能在別的命名空間，不准蓋）', async () => {
  const orig = globalThis.fetch;
  globalThis.fetch = (async () =>
    new Response(JSON.stringify({ success: true, libraries: [], count: 0 }), { status: 200 })) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', LEO_NS), false);
  } finally {
    globalThis.fetch = orig;
  }
});

test('namespaceHasKnowledge：問不到（實例沒起來／舊版沒這條路／網路斷）→ null，不宣稱任何事', async () => {
  const orig = globalThis.fetch;
  globalThis.fetch = (async () => { throw new Error('ECONNREFUSED'); }) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', LEO_NS), null);
  } finally {
    globalThis.fetch = orig;
  }
  globalThis.fetch = (async () => new Response('nope', { status: 500 })) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', LEO_NS), null);
  } finally {
    globalThis.fetch = orig;
  }
});

test('namespaceHasKnowledge：回應形狀不對 → null（讀不出來 ≠ 沒有資料，禁假綠）', async () => {
  const orig = globalThis.fetch;
  globalThis.fetch = (async () =>
    new Response(JSON.stringify({ success: true }), { status: 200 })) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', LEO_NS), null);
  } finally {
    globalThis.fetch = orig;
  }
});

test('namespaceHasKnowledge：缺 url 或缺 namespace → null（不打任何請求）', async () => {
  const orig = globalThis.fetch;
  globalThis.fetch = (async () => { throw new Error('不該被呼叫'); }) as typeof fetch;
  try {
    assert.equal(await namespaceHasKnowledge('', LEO_NS), null);
    assert.equal(await namespaceHasKnowledge('https://cypher.example.dev', ''), null);
  } finally {
    globalThis.fetch = orig;
  }
});
