/**
 * credentials 路由測試（D38 圍牆修復後補寫，2026-08-08）
 *
 * 前身是刻意留紅的 placeholder（見 git history）：2026-08-07 D38 把 credential 目錄從
 * 「獨立 credentials 表 + 原生 SQL」改成「KBDB entries（entry_type='credential'）+
 * HTTP API」，舊測試全部作廢，agent 中途被中斷沒補上，故意留一個會失敗的測試佔位、
 * 避免「no tests」被誤讀成「通過」。本檔依 placeholder 頭部列的五項補齊。
 *
 * 測試手法比照姊妹模組 execution-logger.test.ts：`vi.stubGlobal('fetch', ...)` 攔截，
 * 但這裡的攔截器是**有狀態的假 KBDB**（in-memory entries store），因為 credentials.ts
 * 一次操作常涉及多輪 HTTP 呼叫（find → upsert / find → delete），單次回應的 mock 測不出
 * 「查得到剛寫的」「刪掉後真的查不到」這類語意，需要一個會記狀態的假後端。
 */
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { Hono } from 'hono';
import { credentialsRouter, getCredentialSecretRefs, hasCredential, invalidateCredentialCache } from '../src/routes/credentials';
import type { Bindings } from '../src/types';
// Workers runtime（@cloudflare/vitest-pool-workers）沒有 node:fs——原始碼掃描改用 Vite 的
// `?raw` import 取字串內容（build-time 讀檔，runtime 是純字串，不受 Workers 限制）。
// @ts-expect-error -- vite ?raw 型別由 tsconfig 的 vite/client 提供，非本檔關注重點
import credentialsSource from '../src/routes/credentials.ts?raw';

afterEach(() => vi.unstubAllGlobals());

// ── 有狀態假 KBDB：只實作 credentials.ts 實際會打的四個操作（GET list/find, POST, PATCH, DELETE）──
interface FakeEntry {
  id: string;
  entry_type: string;
  owner_id: string;
  page_name: string;
  metadata_json: string;
  created_at: number;
}

function makeFakeKbdb() {
  const entries: FakeEntry[] = [];
  let idSeq = 0;
  const secretsStore = new Map<string, string>(); // secretRef -> plaintext（模擬 CF Workers Secrets，唯寫，測試用來斷言「有沒有被塞值」）
  const secretPuts: Array<{ name: string; text: string }> = [];
  const secretDeletes: string[] = [];
  const kbdbRequests: Array<{ method: string; url: string; body: unknown }> = [];

  async function handle(url: string, init: RequestInit = {}): Promise<Response> {
    const method = (init.method ?? 'GET').toUpperCase();
    const u = new URL(url);

    // CF Workers Scripts secrets 管理 API（唯寫，讀不回值）
    if (u.hostname === 'api.cloudflare.com') {
      if (method === 'PUT' && u.pathname.endsWith('/secrets')) {
        const body = JSON.parse(String(init.body)) as { name: string; text: string };
        secretsStore.set(body.name, body.text);
        secretPuts.push(body);
        return new Response(JSON.stringify({ success: true }), { status: 200 });
      }
      if (method === 'DELETE' && u.pathname.includes('/secrets/')) {
        const name = u.pathname.split('/secrets/')[1];
        secretsStore.delete(name);
        secretDeletes.push(name);
        return new Response(JSON.stringify({ success: true }), { status: 200 });
      }
      throw new Error(`unhandled CF API call: ${method} ${url}`);
    }

    // KBDB entries API
    kbdbRequests.push({ method, url, body: init.body ? JSON.parse(String(init.body)) : undefined });

    if (method === 'POST' && u.pathname === '/entries') {
      const body = JSON.parse(String(init.body)) as Partial<FakeEntry>;
      const entry: FakeEntry = {
        id: `e_${++idSeq}`,
        entry_type: body.entry_type!,
        owner_id: body.owner_id!,
        page_name: body.page_name!,
        metadata_json: body.metadata_json!,
        created_at: Math.floor(Date.now() / 1000),
      };
      entries.push(entry);
      return new Response(JSON.stringify({ success: true, entry }), { status: 200 });
    }

    if (method === 'GET' && u.pathname === '/entries') {
      const ownerId = u.searchParams.get('owner_id');
      const entryType = u.searchParams.get('entry_type');
      const pageName = u.searchParams.get('page_name');
      let rows = entries.filter((e) => e.entry_type === entryType && e.owner_id === ownerId);
      if (pageName) rows = rows.filter((e) => e.page_name === pageName);
      return new Response(JSON.stringify({ success: true, entries: rows, count: rows.length }), { status: 200 });
    }

    if (method === 'PATCH' && u.pathname.startsWith('/entries/')) {
      const id = decodeURIComponent(u.pathname.slice('/entries/'.length));
      const body = JSON.parse(String(init.body)) as Partial<FakeEntry>;
      const entry = entries.find((e) => e.id === id);
      if (!entry) return new Response(JSON.stringify({ success: false }), { status: 404 });
      if (body.metadata_json !== undefined) entry.metadata_json = body.metadata_json;
      return new Response(JSON.stringify({ success: true, entry }), { status: 200 });
    }

    if (method === 'DELETE' && u.pathname.startsWith('/entries/')) {
      const id = decodeURIComponent(u.pathname.slice('/entries/'.length));
      const idx = entries.findIndex((e) => e.id === id);
      if (idx === -1) return new Response(JSON.stringify({ success: false }), { status: 404 });
      entries.splice(idx, 1); // 真的從陣列移除，不是標記
      return new Response(JSON.stringify({ success: true }), { status: 200 });
    }

    throw new Error(`unhandled KBDB call: ${method} ${url}`);
  }

  vi.stubGlobal('fetch', vi.fn((url: string, init?: RequestInit) => handle(url, init)));

  return { entries, secretsStore, secretPuts, secretDeletes, kbdbRequests };
}

function fakeEnv(): Bindings {
  return {
    KBDB_BASE_URL: 'https://kbdb.test',
    CF_SECRETS_API_TOKEN: 'fake-cf-token',
    CF_ACCOUNT_ID: 'fake-account',
    ENVIRONMENT: 'test',
    CREDENTIALS_KV: { delete: vi.fn(async () => {}) } as unknown as KVNamespace,
  } as unknown as Bindings;
}

function app() {
  const a = new Hono<{ Bindings: Bindings }>();
  a.route('/', credentialsRouter);
  return a;
}

beforeEach(() => {
  invalidateCredentialCache('tenant-a');
  invalidateCredentialCache('tenant-b');
});

describe('1. 寫入走 KBDB HTTP API，且 owner_id = api_key（租戶隔離）', () => {
  it('POST /credentials 寫入後，entries 裡的 owner_id 就是呼叫者的 api_key', async () => {
    const fake = makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    const res = await a.request('/credentials', {
      method: 'POST',
      headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'telegram_bot_token', value: 'secret-plaintext-value', service: 'telegram' }),
    }, env);
    expect(res.status).toBe(200);
    const body = (await res.json()) as { success: boolean };
    expect(body.success).toBe(true);
    expect(fake.entries).toHaveLength(1);
    expect(fake.entries[0].owner_id).toBe('tenant-a');
    expect(fake.entries[0].page_name).toBe('telegram_bot_token');
  });

  it('兩個不同 api_key 各自建立的同名 credential 落在不同 owner_id、互不覆蓋', async () => {
    const fake = makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'gemini_api_key', value: 'value-a' }),
    }, env);
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-b', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'gemini_api_key', value: 'value-b' }),
    }, env);
    expect(fake.entries).toHaveLength(2);
    const owners = fake.entries.map((e) => e.owner_id).sort();
    expect(owners).toEqual(['tenant-a', 'tenant-b']);
  });
});

describe('2. 讀取查得回 secret_ref，且查不到別的租戶的', () => {
  it('getCredentialSecretRefs 回該租戶的 name→secret_ref 對照，不含其他租戶的', async () => {
    makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'gemini_api_key', value: 'value-a' }),
    }, env);
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-b', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'other_key', value: 'value-b' }),
    }, env);

    const refsA = await getCredentialSecretRefs(env, 'tenant-a');
    expect(Object.keys(refsA)).toEqual(['gemini_api_key']);
    expect(refsA.gemini_api_key).toMatch(/^CRED_GEMINI_API_KEY_/);
    expect(refsA.other_key).toBeUndefined(); // 查不到別租戶的

    const refsB = await getCredentialSecretRefs(env, 'tenant-b');
    expect(Object.keys(refsB)).toEqual(['other_key']);
  });

  it('hasCredential：查得到自己的，查不到別租戶的同名 credential', async () => {
    makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'kbdb_internal_token', value: 'v' }),
    }, env);
    expect(await hasCredential(env, 'tenant-a', 'kbdb_internal_token')).toBe(true);
    expect(await hasCredential(env, 'tenant-b', 'kbdb_internal_token')).toBe(false);
  });
});

describe('3. 刪除是真的刪（不是 deprecated 標記）', () => {
  it('DELETE /credentials/:name 後，該筆 entries row 從 KBDB 消失（不是 metadata 打 deprecated 標記）', async () => {
    const fake = makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'to_delete', value: 'v' }),
    }, env);
    expect(fake.entries).toHaveLength(1);

    const res = await a.request('/credentials/to_delete', {
      method: 'DELETE', headers: { 'X-Arcrun-API-Key': 'tenant-a' },
    }, env);
    expect(res.status).toBe(200);
    const body = (await res.json()) as { success: boolean; source: string };
    expect(body.success).toBe(true);
    expect(body.source).toBe('workers-secrets');

    // 真的從陣列移除，不是留著、metadata 打上 status:deprecated
    expect(fake.entries).toHaveLength(0);
    // Workers Secret 本體也真的被刪（DELETE 呼叫過），不是只刪目錄留孤兒密文
    expect(fake.secretDeletes.length).toBe(1);
  });
});

describe('4. 零原生 SQL：整支檔案不得出現 .prepare/.exec/.batch', () => {
  it('routes/credentials.ts 原始碼掃描：沒有任何 D1 原生呼叫語法', () => {
    expect(/\.\s*(prepare|exec|batch)\s*\(/.test(credentialsSource)).toBe(false);
  });
});

describe('5. 密文本體不落 KBDB（只有 secret_ref 指標）—— D19 不變', () => {
  it('送去 KBDB 的 body 裡從頭到尾沒有明文 credential value，只有 secret_ref', async () => {
    const fake = makeFakeKbdb();
    const env = fakeEnv();
    const a = app();
    const plaintext = 'super-secret-plaintext-should-never-leave-workers-secrets';
    await a.request('/credentials', {
      method: 'POST', headers: { 'X-Arcrun-API-Key': 'tenant-a', 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'sensitive_key', value: plaintext }),
    }, env);

    // 明文只出現在 CF Workers Secrets 的 PUT（唯寫 API），不出現在任何打去 KBDB 的請求 body 裡
    expect(fake.secretPuts.some((p) => p.text === plaintext)).toBe(true);
    for (const req of fake.kbdbRequests) {
      expect(JSON.stringify(req.body ?? '')).not.toContain(plaintext);
    }
    // entries 裡存的是 secret_ref 指標，不是值
    expect(fake.entries[0].metadata_json).not.toContain(plaintext);
    expect(fake.entries[0].metadata_json).toContain('secret_ref');
  });
});
