// t115 二修 — kbdb auth guard tests (fail-closed behaviour).
//
// Token NOT set:
//   - GET / and GET /health → 200 (health exempt)
//   - GET /entries → 200 + console.warn (reads pass during upgrade window)
//   - POST/PATCH/DELETE /entries → 401 (fail-closed for writes)
//
// Token SET:
//   - / and /health → 200 (health always exempt)
//   - missing / wrong / no-Bearer prefix → 401
//   - correct Bearer → 200
import { describe, it, expect } from 'vitest';
import { Hono } from 'hono';
import type { Bindings } from '../src/types';

// ⚠️ 這裡曾經「複製一份 index.ts 的 middleware」來測——複本會與真實作漂移，
// 測綠了也不代表線上安全（總管 07-28 三修時發現：真 app 已改 fail-closed，複本還放行讀取）。
// 現在改成：把真 middleware 從 src/index.ts 匯入無法做到（app 已組裝好路由），
// 故改為「複本必須與 src/index.ts 的行為斷言一致」＋一條結構測試（見最下方 test）。
function makeApp(token?: string) {
  const app = new Hono<{ Bindings: Bindings }>();

  app.use('*', async (c, next) => {
    const path = new URL(c.req.url).pathname;
    if (path === '/' || path === '/health') return next();
    const envToken = c.env.KBDB_INTERNAL_TOKEN;
    if (!envToken) return c.json({ error: 'Unauthorized', detail: 'kbdb 尚未設定內部金鑰，請重跑安裝器' }, 401);
    const auth = c.req.header('Authorization');
    if (!auth || auth !== `Bearer ${envToken}`) return c.json({ error: 'Unauthorized' }, 401);
    return next();
  });

  app.get('/', (c) => c.json({ status: 'ok' }));
  app.get('/health', (c) => c.json({ ok: true }));
  app.get('/entries', (c) => c.json({ success: true, entries: [] }));
  app.post('/entries', async (c) => c.json({ success: true }));
  app.patch('/entries/:id', async (c) => c.json({ success: true }));
  app.delete('/entries/:id', async (c) => c.json({ success: true }));

  // Bind the token into the env for every request.
  const original = app.fetch.bind(app);
  return (req: Request) =>
    original(req, { DB: {} as D1Database, ENVIRONMENT: 'test', KBDB_INTERNAL_TOKEN: token } as Bindings, {});
}

describe('kbdb auth guard — token NOT set', () => {
  const fetch = makeApp(undefined);

  it('GET / passes (health exempt)', async () => {
    const res = await fetch(new Request('http://kbdb/'));
    expect(res.status).toBe(200);
  });

  it('GET /health passes (health exempt)', async () => {
    const res = await fetch(new Request('http://kbdb/health'));
    expect(res.status).toBe(200);
  });

  it('GET /entries 也被拒（fail-closed：讀取放行＝洞沒補，t115 三修）', async () => {
    const res = await fetch(new Request('http://kbdb/entries'));
    expect(res.status).toBe(401);
  });

  it('POST /entries without token → 401 (fail-closed for writes)', async () => {
    const res = await fetch(new Request('http://kbdb/entries', { method: 'POST' }));
    expect(res.status).toBe(401);
    const body = await res.json() as { error: string };
    expect(body.error).toBe('Unauthorized');
  });

  it('PATCH /entries/x without token → 401 (fail-closed for writes)', async () => {
    const res = await fetch(new Request('http://kbdb/entries/x', { method: 'PATCH' }));
    expect(res.status).toBe(401);
  });

  it('DELETE /entries/x without token → 401 (fail-closed for writes)', async () => {
    const res = await fetch(new Request('http://kbdb/entries/x', { method: 'DELETE' }));
    expect(res.status).toBe(401);
  });
});

describe('kbdb auth guard — token SET', () => {
  const SECRET = 'test-secret-abc123';
  const fetch = makeApp(SECRET);

  it('GET / always passes (health exempt)', async () => {
    const res = await fetch(new Request('http://kbdb/'));
    expect(res.status).toBe(200);
  });

  it('GET /health always passes (health exempt)', async () => {
    const res = await fetch(new Request('http://kbdb/health'));
    expect(res.status).toBe(200);
  });

  it('GET /entries without Authorization → 401', async () => {
    const res = await fetch(new Request('http://kbdb/entries'));
    expect(res.status).toBe(401);
    const body = await res.json() as { error: string };
    expect(body.error).toBe('Unauthorized');
  });

  it('GET /entries with wrong token → 401', async () => {
    const res = await fetch(
      new Request('http://kbdb/entries', {
        headers: { Authorization: 'Bearer wrong-token' },
      }),
    );
    expect(res.status).toBe(401);
  });

  it('GET /entries with Bearer prefix missing → 401', async () => {
    const res = await fetch(
      new Request('http://kbdb/entries', {
        headers: { Authorization: SECRET },
      }),
    );
    expect(res.status).toBe(401);
  });

  it('GET /entries with correct Bearer token → 200', async () => {
    const res = await fetch(
      new Request('http://kbdb/entries', {
        headers: { Authorization: `Bearer ${SECRET}` },
      }),
    );
    expect(res.status).toBe(200);
  });

  it('POST /entries with correct Bearer token → 200', async () => {
    const res = await fetch(
      new Request('http://kbdb/entries', {
        method: 'POST',
        headers: { Authorization: `Bearer ${SECRET}` },
      }),
    );
    expect(res.status).toBe(200);
  });

  it('POST /entries without token → 401', async () => {
    const res = await fetch(
      new Request('http://kbdb/entries', { method: 'POST' }),
    );
    expect(res.status).toBe(401);
  });
});

// 結構閘（總管 07-28 加）：src/index.ts 的 guard 必須是 fail-closed——
// 無 token 時不得有任何「return next()」的放行分支（health 豁免除外）。
// 這條擋的是「測試複本與真實作漂移」那類假綠。
import { readFileSync } from 'node:fs';
describe('t115 結構閘：真實作必須 fail-closed', () => {
  it('src/index.ts 無 token 分支不放行', () => {
    const src = readFileSync(new URL('../src/index.ts', import.meta.url), 'utf8');
    const guard = src.slice(src.indexOf("app.use('*'"), src.indexOf("app.get('/', "));
    const noTokenBlock = guard.slice(guard.indexOf('if (!token)'), guard.indexOf('const auth'));
    expect(noTokenBlock).toContain('401');
    expect(noTokenBlock).not.toContain('return next()');
  });
});
