/**
 * 兩條路必須得出同一個答案 —— 本票的核心驗收。
 *
 * leo 2026-08-12：「根本就不應該在 CLI，我要的是一個大家都可以用到的規則。」
 *
 * 後果已經真的發生過：`acr` 那條有 Arcrun#97 的修法、安裝器那條沒有，
 * 於是安裝器照名字找、找不到就建一顆空的綁上去 ⇒ 使用者的工作流與登入狀態整片消失。
 *
 * 這份測試把**同一個帳號狀態**餵給兩條路：
 *   A. `acr` 那條：`CfAccountClient` + `resource-resolver`（CLI 真正跑的 import 鏈）
 *   B. 安裝器那條：只 import `shared/resource-rule/`（安裝器唯一該碰的入口）
 * 然後比對它們選出的 **resource id 必須相同**。
 *
 * 假的是 `fetch`，不是 `ResourceApi`——所以兩條路都真的走完 HTTP → 解析 → 判斷整條鏈。
 * 只測判斷會漏掉「怎麼把 CF 回應讀成事實」，而 #97 的重演只要眼睛不一樣就夠了。
 */

import { test } from 'node:test';
import assert from 'node:assert/strict';

// ── A：acr 那條（CLI 真正用的東西）
import { CfAccountClient } from '../src/lib/cf-api.ts';
import { planResources, applyResourcePlan, bindingKey } from '../src/lib/resource-resolver.ts';
import type { BindingRequirement } from '../src/lib/resource-resolver.ts';

// ── B：安裝器那條（只碰 shared/）
import { resolveInstanceResources } from '../../shared/resource-rule/installer-entry.mjs';

// ── 共用 fixture
import {
  makeAccount,
  requirements,
  SCENARIOS,
  WORKER_NEEDS,
  type Scenario,
} from '../../shared/resource-rule/tests/fixture-account.mjs';

const ACCOUNT = 'acct-fixture';
const TOKEN = 'tok-fixture';

/** 把 fixture 的需求組成安裝器吃的 wrangler.toml 文字（它的入口是從 toml 讀需求的）。 */
function tomlsFor(): string[] {
  return Object.entries(WORKER_NEEDS).map(([script, need]) => {
    let t = `name = "${script}"\ncompatibility_date = "2025-02-19"\n`;
    for (const b of need.kv) t += `\n[[kv_namespaces]]\nbinding = "${b}"\nid = "PLACEHOLDER"\n`;
    for (const d of need.d1) {
      t += `\n[[d1_databases]]\nbinding = "${d.binding}"\ndatabase_name = "${d.database_name}"\ndatabase_id = "PLACEHOLDER"\n`;
    }
    return t;
  });
}

/** A：跑 acr 那條。CfAccountClient 走 global fetch，所以這裡把它換成 fixture。 */
async function runAcrPath(scenario: Scenario, mode: 'update' | 'init') {
  const account = makeAccount(scenario);
  const realFetch = globalThis.fetch;
  globalThis.fetch = account.fetch;
  try {
    const api = new CfAccountClient(ACCOUNT, TOKEN);
    const plan = await planResources(api, requirements() as BindingRequirement[], mode);
    if (plan.blockers.length > 0) {
      return { blocked: true, blockers: plan.blockers, bindings: {} as Record<string, string>, account };
    }
    const resolved = await applyResourcePlan(api, plan);
    const bindings: Record<string, string> = {};
    for (const [k, r] of resolved) bindings[k] = r.value;
    return { blocked: false, blockers: [] as string[], bindings, account };
  } finally {
    globalThis.fetch = realFetch;
  }
}

/** B：跑安裝器那條。只用 shared/ 的入口，fetch 直接注入。 */
async function runInstallerPath(scenario: Scenario, mode: 'update' | 'init') {
  const account = makeAccount(scenario);
  const r = await resolveInstanceResources({
    accountId: ACCOUNT,
    apiToken: TOKEN,
    wranglerTomls: tomlsFor(),
    mode,
    fetch: account.fetch,
  });
  return { blocked: r.blocked, blockers: r.blockers, bindings: r.bindings, account };
}

/** 把兩邊的決定印出來——PR 要貼的就是這張對照表。 */
function report(scenario: Scenario, a: Record<string, string>, b: Record<string, string>): void {
  const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  console.log(`\n  ── ${scenario}：${SCENARIOS[scenario].label}`);
  console.log(`     ${'binding'.padEnd(34)} ${'acr 選的'.padEnd(24)} 安裝器選的        一致?`);
  for (const k of keys) {
    const same = a[k] === b[k] ? '✓' : '✗';
    console.log(`     ${k.padEnd(34)} ${(a[k] ?? '—').padEnd(24)} ${(b[k] ?? '—').padEnd(18)} ${same}`);
  }
}

// ─────────────────────────────────────────────────────────────────────────────

for (const scenario of ['fresh', 'installed', 'renamed'] as const) {
  const mode = scenario === 'fresh' ? 'init' : 'update';

  test(`兩條路一致 — ${scenario}：${SCENARIOS[scenario].label}`, async () => {
    const a = await runAcrPath(scenario, mode);
    const b = await runInstallerPath(scenario, mode);

    assert.equal(a.blocked, b.blocked, '一邊停手、一邊照做 = 最危險的分歧');
    assert.deepEqual(a.blockers, b.blockers, '停手的理由也要一樣');
    report(scenario, a.bindings, b.bindings);
    assert.deepEqual(
      a.bindings,
      b.bindings,
      `${scenario}：兩條路選出的 resource id 不同——這就是 Arcrun#97 的形狀`,
    );

    // 建立行為也要一致（一邊沿用、一邊新建 = 使用者的東西在其中一條路上會消失）
    assert.deepEqual(a.account.created, b.account.created, '兩條路「建了什麼」必須一樣');
  });
}

// ── 三種情境各自該有的行為（不只是「兩邊一樣」，還要「一樣地對」）─────────────

test('情境① 沒裝過 → 正常建新的（不能為了沿用而變成永遠不建）', async () => {
  const { blocked, bindings, account } = await runInstallerPath('fresh', 'init');
  assert.equal(blocked, false, '全新帳號要裝得起來');
  assert.equal(account.created.kv.length, 9, `應新建 9 顆 KV，實際 ${account.created.kv.length}`);
  assert.equal(account.created.d1.length, 1, `應新建 1 顆 D1，實際 ${account.created.d1.length}`);
  // cypher 的 CREDENTIALS_DB 與 kbdb 的 DB 宣告同一個 database_name → 只該建一顆，兩邊共用
  assert.equal(bindings['d1:CREDENTIALS_DB'], bindings['d1:DB'], '同一顆 D1 不該被建成兩顆');
  console.log(`\n  ① 新建：KV ${account.created.kv.length} 顆、D1 ${account.created.d1.length} 顆` +
    `（D1 共用：CREDENTIALS_DB = DB = ${bindings['d1:DB']}）`);
});

test('情境② 裝過了 → 沿用原本那幾顆，工作流與登入 session 都還在', async () => {
  const { blocked, bindings, account } = await runInstallerPath('installed', 'update');
  assert.equal(blocked, false);
  assert.deepEqual(account.created, { kv: [], d1: [], vectorize: [] }, '更新不該建出任何新資源');

  // 使用者的東西掛在資源 id 上：綁定還指向原本那顆 = 東西還在
  assert.equal(bindings['kv_namespace:WEBHOOKS'], account.kvIdFor('WEBHOOKS'));
  assert.equal(bindings['kv_namespace:SESSIONS_KV'], account.kvIdFor('SESSIONS_KV'));
  assert.equal(bindings['d1:DB'], account.d1Id);
  console.log(`\n  ② 沿用：WEBHOOKS → ${bindings['kv_namespace:WEBHOOKS']}` +
    `（工作流 ${account.userData.workflows.length} 支還在）｜` +
    `SESSIONS_KV → ${bindings['kv_namespace:SESSIONS_KV']}（登入 session 還在）｜` +
    `DB → ${bindings['d1:DB']}（子庫 ${account.userData.libraries.length} 個還在）｜新建 0 顆`);
});

test('情境③ 資源在但名字與預期完全不同 → 仍然沿用（#97 的病根，專門驗）', async () => {
  const { blocked, bindings, account } = await runInstallerPath('renamed', 'update');
  assert.equal(blocked, false);
  assert.deepEqual(account.created, { kv: [], d1: [], vectorize: [] },
    '名字對不上就新建 = 正是 #97：一次更新生出 9 顆空 KV，使用者的東西從畫面上消失');
  for (const b of ['WEBHOOKS', 'SESSIONS_KV', 'RECIPES', 'USERS_KV']) {
    assert.equal(bindings[bindingKey('kv_namespace', b)], account.kvIdFor(b),
      `${b} 沒有沿用到原本那顆`);
  }
  console.log(`\n  ③ 名字全不同（例：WEBHOOKS 那顆實際叫 "${SCENARIOS.renamed.titleFor('WEBHOOKS')}"）` +
    ` → 仍沿用 ${bindings['kv_namespace:WEBHOOKS']}，新建 0 顆`);
});
