/**
 * 「靜態租戶字串不得用於資料面過濾」這道閘**自己**的測試（Arcrun#108）。
 *
 * 收工標準明列三條，這裡逐條釘：
 *   ① 會擋，不是只提醒 → 壞例子必須產出違規（CLI 端據此 exit 1、hook 據此 exit 2）
 *   ② 判準看「有沒有在做那件事」 → 一整組「長得像但沒在做」的合法寫法必須零誤攔
 *   ③ 閘自己要能被測試 → 規則是純函式，這裡直接餵字串；不需要跑檔案系統、也擋不到自己
 *
 * 外加一條回歸：現行 src/ 必須是乾淨的（`?raw` 讀真原始碼，不是讀我編的假字串）。
 */
import { describe, it, expect } from 'vitest';
// @ts-expect-error -- 純規則模組（.mjs，零 node 相依），型別非本檔關注重點
import { scanSource, TENANT_SOURCE_FILE } from '../scripts/tenant-source-rules.mjs';
// @ts-expect-error -- vite ?raw：build-time 讀檔，runtime 是純字串（Workers 沒有 node:fs）
import tenantLibSource from '../src/lib/tenant.ts?raw';
// @ts-expect-error -- 同上
import portalDataSource from '../src/routes/portal-data.ts?raw';
// @ts-expect-error -- 同上
import portalSource from '../src/routes/portal.ts?raw';
// @ts-expect-error -- 同上
import consoleAuthSource from '../src/routes/console-auth.ts?raw';
// @ts-expect-error -- 同上
import consoleDashboardSource from '../src/routes/console-dashboard.ts?raw';

type Violation = { rule: string; line: number; text: string; message: string };
const scan = (path: string, text: string): Violation[] => scanSource(path, text) as Violation[];
const rulesOf = (v: Violation[]): string[] => [...new Set(v.map((x) => x.rule))].sort();

const FILE = 'src/routes/portal-data.ts';

describe('閘會擋：三種「靜態租戶字串進資料面」的真實形狀', () => {
  it('T1 — 在 tenant.ts 以外讀租戶環境變數（#105/#108 的原句）', () => {
    const bad = `export function portalTenant(env: Bindings): string {\n  return env.CONSOLE_TENANT || 'leo';\n}`;
    const v = scan('src/routes/portal.ts', bad);
    expect(rulesOf(v)).toContain('T1');
    expect(v[0].message).toContain('knowledgeOwner');
  });

  it('T1 — `c.env.ARCRUN_NAMESPACE` 也一樣（換一個變數名不是換一個做法）', () => {
    const v = scan('src/routes/console-dashboard.ts', `const t = c.env.ARCRUN_NAMESPACE || 'leo';`);
    expect(rulesOf(v)).toEqual(['T1']);
  });

  it('T2 — 繞過唯一產地自己 cast 一個 TenantId', () => {
    const v = scan(FILE, `const tenant = (c.env.SOMETHING ?? '') as TenantId;`);
    expect(rulesOf(v)).toContain('T2');
  });

  it('T2 — 連在 tenant.ts 裡都不准把「字面字串」當成租戶識別（那就是 `|| "leo"` 的原形）', () => {
    const v = scan(TENANT_SOURCE_FILE, `  return 'leo' as TenantId;`);
    expect(rulesOf(v)).toEqual(['T2']);
    expect(v[0].message).toContain('TenantUnresolvedError');
  });

  it('T3 — 拿帳號層字串去組知識資料面的 owner_id（#108 那一行，逐字）', () => {
    const bad = "    const res = await kbdbFetch(c.env, `/map?owner_id=${encodeURIComponent(portalTenant(c.env))}`);";
    const v = scan(FILE, bad);
    expect(rulesOf(v)).toContain('T3');
    expect(v[0].message).toContain('1854');
  });

  it('T3 — 換成物件屬性寫法一樣擋（`owner_id: portalTenant(c.env)`）', () => {
    const v = scan(FILE, `      body: JSON.stringify({ values, owner_id: portalTenant(c.env) }),`);
    expect(rulesOf(v)).toContain('T3');
  });

  it('T3 — accountTenant() 這個新名字也擋（規則盯的是「這是帳號層的值」，不是某個函式名字的拼法）', () => {
    const v = scan(FILE, "  kbdbFetch(env, `/entries?owner_id=${accountTenant(env)}`);");
    expect(rulesOf(v)).toContain('T3');
  });
});

describe('閘不誤攔：長得像、但沒有在做那件事的合法寫法', () => {
  const legit: [string, string, string][] = [
    ['讀取別人回傳的 owner_id（不是在組過濾）', FILE, `    if (!isOwnedBy(entry.owner_id, knowledgeOwner(c.env))) return notFound(c);`],
    ['型別宣告裡的 owner_id 欄位', FILE, `      | { record?: { values?: Record<string, unknown>; owner_id?: string | null } }`],
    ['走唯一入口組過濾', FILE, "    const res = await kbdbFetch(c.env, `/map?${ownerQuery(tenant)}`);"],
    ['走唯一入口填 body', FILE, `      body: JSON.stringify({ template, values, owner_id: ownerField(tenant) }),`],
    ['帳號子 namespace 的過濾（`{tenant}::portal`，那是 cypher 自己寫的資料）', 'src/routes/portal.ts', `  const res = await kbdbFetch(env, \`/records/by-template/x?owner_id=\${encodeURIComponent(ns)}\`);`],
    ['請求自帶的租戶（webhooks-named 慣例：呼叫端就是租戶）', 'src/routes/webhooks-named.ts', `      owner_id: apiKey,`],
    ['註解裡整句在講 CONSOLE_TENANT 與 owner_id（文件不是行為）', FILE, `// 之前的病：owner_id 拿 env.CONSOLE_TENANT，portalTenant(c.env) 那條路整個空掉`],
    ['JSDoc 區塊裡出現同樣的字', FILE, ` * 舊寫法 owner_id=\${portalTenant(env)} 已廢除，改走 knowledgeOwner。`],
    ['行末註解裡出現（程式碼本身乾淨）', FILE, `    const tenant = knowledgeOwner(c.env); // 不是 portalTenant(c.env)，也不是 owner_id=leo`],
    ['tenant.ts 自己讀環境變數（它就是唯一產地）', TENANT_SOURCE_FILE, `  const injected = (env.ARCRUN_NAMESPACE ?? '').trim();`],
    ['types.ts 只宣告型別不取值', 'src/types.ts', `  CONSOLE_TENANT?: string;`],
  ];

  for (const [name, path, line] of legit) {
    it(`零誤攔：${name}`, () => {
      expect(scan(path, line)).toEqual([]);
    });
  }
});

describe('回歸：現行原始碼是乾淨的（讀真檔，不是讀我編的字串）', () => {
  const files: [string, string][] = [
    [TENANT_SOURCE_FILE, tenantLibSource as string],
    ['src/routes/portal-data.ts', portalDataSource as string],
    ['src/routes/portal.ts', portalSource as string],
    ['src/routes/console-auth.ts', consoleAuthSource as string],
    ['src/routes/console-dashboard.ts', consoleDashboardSource as string],
  ];
  for (const [path, text] of files) {
    it(`${path} 零違規`, () => {
      expect(scan(path, text)).toEqual([]);
    });
  }

  it('唯一產地本身沒有字面預設值（knowledgeOwner 解析不到要用丟的，不是回 "leo"）', () => {
    const body = (tenantLibSource as string).slice(
      (tenantLibSource as string).indexOf('export function knowledgeOwner'),
      (tenantLibSource as string).indexOf('export function tenantFromApiKey'),
    );
    expect(body).toContain('TenantUnresolvedError');
    // 解析路徑只准回 env 讀到的值；任何 `|| '...'` / `?? '...'` 形式的字面 fallback 都是本票的病本身
    expect(body).not.toMatch(/(\|\||\?\?)\s*['"][^'"]+['"]/);
  });
});
