/**
 * GET|POST /portal/admin/ai —— arcrun-rag#10 迴歸守衛
 *
 * 🔴 為什麼有這支測試（別刪）：
 *   這條 route **以前根本不存在**，但前端設定頁一直在打它 ⇒ 用戶填 Gemini key → 404
 *   ⇒ **key 從來沒被存進任何地方**，畫面卻像存好了（藍字＝假綠）。
 *   leo 實撞成「重裝後 key 不見」，真相是「從來沒存進去，所以重填也沒用」。
 *   產物層鐵證（修復前）：bundle tier2/ui grep 'portal/admin/ai'=1、tier2/cypher=**0**。
 *   ⇒ 這支測試的存在本身就是防線：**route 消失＝測試紅**。
 *
 * 覆蓋：
 *   1. 未登入 → 401；非 admin → 403（不是 404＝route 真的在）
 *   2. GET 回 has_key 布林，**永不回傳 key 本身**（D36）
 *   3. POST 空 body → 400（不假裝成功）
 *   4. POST 只改 Claude 偏好（不帶 key）→ 成功，且不碰 credential
 */
import { SELF, env, fetchMock } from 'cloudflare:test';
import { beforeAll, afterEach, describe, it, expect } from 'vitest';
import { hashPassword } from '../src/lib/portal-auth';

const KBDB = 'https://kbdb.test';

let storedHash: string;

beforeAll(async () => {
  fetchMock.activate();
  fetchMock.disableNetConnect();
  storedHash = await hashPassword('unit-test-pw-1', 10_000);
});
afterEach(() => fetchMock.assertNoPendingInterceptors());

function json(method: string, path: string, body?: unknown, headers: Record<string, string> = {}) {
  return SELF.fetch(`http://localhost${path}`, {
    method,
    headers: { 'Content-Type': 'application/json', ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
}

function mockGetRecord(recordId: string, values: Record<string, string>) {
  fetchMock
    .get(KBDB)
    .intercept({ path: `/records/${recordId}`, method: 'GET' })
    .reply(200, { success: true, record: { record_id: recordId, template_id: 'tpl_pu', values } });
}

function adminValues(overrides: Record<string, string> = {}): Record<string, string> {
  return {
    email: 'admin@example.com',
    display_name: '管理員',
    status: 'active',
    role: 'admin',
    password_hash: storedHash,
    libraries: '["*"]',
    created_at: '2026-07-14T00:00:00.000Z',
    updated_at: '2026-07-14T00:00:00.000Z',
    ...overrides,
  };
}

async function seedSession(token: string, recordId: string) {
  await env.SESSIONS_KV.put(`portal_sess:${token}`, JSON.stringify({ record_id: recordId }));
}

const authHdr = (t: string) => ({ Authorization: `Bearer ${t}` });

describe('GET /portal/admin/ai — 認證閘（route 存在的證明）', () => {
  it('未登入 → 401（不是 404 ⇒ route 真的在）', async () => {
    const res = await json('GET', '/portal/admin/ai');
    expect(res.status).toBe(401);
    expect(res.status).not.toBe(404);
  });

  it('非 admin → 403', async () => {
    await seedSession('tok-user', 'rec_user');
    mockGetRecord('rec_user', adminValues({ role: 'user', email: 'u@example.com' }));
    const res = await json('GET', '/portal/admin/ai', undefined, authHdr('tok-user'));
    expect(res.status).toBe(403);
  });
});

describe('GET /portal/admin/ai — 回應形狀（D36：永不回傳 key）', () => {
  it('回 has_key 布林，且回應完全不含金鑰值', async () => {
    await seedSession('tok-a1', 'rec_admin');
    mockGetRecord('rec_admin', adminValues());
    const res = await json('GET', '/portal/admin/ai', undefined, authHdr('tok-a1'));
    expect(res.status).toBe(200);
    const raw = await res.text();
    const d = JSON.parse(raw) as Record<string, unknown>;

    expect(typeof d.has_key).toBe('boolean');

    // D36：回應裡不得出現任何疑似金鑰的欄位
    expect(raw).not.toContain('gemini_api_key_value');
    expect(d).not.toHaveProperty('key');
    expect(d).not.toHaveProperty('value');
    expect(d).not.toHaveProperty('secret_ref');
  });

  // t176 回歸守衛（leo 08-03）：雲端不再有「地端用哪個模型」的概念。
  // 這兩個欄位若復活，代表又走回「雲端控制地端」的老路——那正是 08-03 事故根因
  //（extractor_config 全租戶共用一把，任一處設 claude 就讓所有人萃取全滅）。
  it('不再回 claude_available／use_claude_for_extract（地端模型改由小幫手自己設）', async () => {
    await seedSession('tok-a1b', 'rec_admin');
    mockGetRecord('rec_admin', adminValues());
    const res = await json('GET', '/portal/admin/ai', undefined, authHdr('tok-a1b'));
    const d = (await res.json()) as Record<string, unknown>;
    expect(d).not.toHaveProperty('claude_available');
    expect(d).not.toHaveProperty('use_claude_for_extract');
  });
});

describe('POST /portal/admin/ai — 不假裝成功', () => {
  it('空 body（沒帶金鑰）→ 400，不回 success', async () => {
    await seedSession('tok-a2', 'rec_admin');
    mockGetRecord('rec_admin', adminValues());
    const res = await json('POST', '/portal/admin/ai', {}, authHdr('tok-a2'));
    expect(res.status).toBe(400);
    const d = (await res.json()) as Record<string, unknown>;
    expect(d.success).toBeUndefined();
    expect(String(d.error)).toContain('沒有要變更');
  });

  // t176 回歸守衛：只送 Claude 偏好＝沒有要變更的項目 → 400（該欄位已不存在）。
  it('只送 use_claude_for_extract（已廢欄位）→ 400，不得假裝成功', async () => {
    await seedSession('tok-a3', 'rec_admin');
    mockGetRecord('rec_admin', adminValues());
    const res = await json('POST', '/portal/admin/ai', { use_claude_for_extract: true }, authHdr('tok-a3'));
    expect(res.status).toBe(400);
    const d = (await res.json()) as Record<string, unknown>;
    expect(d.success).toBeUndefined();
  });
});
