/**
 * APIS-052 — Gateway proxy auth boundary.
 *
 * The gateway proxies LLM traffic. gateway-mount.test.ts covers path
 * classification + config equivalence, but NOT the authentication of proxied
 * requests. An unauthenticated request slipping past the gateway's auth layer
 * would expose backend LLM spend.
 *
 * This file asserts the boundary at the gateway's real auth middleware
 * (`buildAuthMiddleware` from src/gateway/auth.ts — the exact handler the
 * gateway sub-app installs ahead of the proxy). We mount it on a bare express
 * app in front of a SENTINEL "backend" handler that records whether it was
 * reached. No real LLM call is made.
 *
 *   - missing Bearer            → 401, backend NOT reached (no passthrough)
 *   - malformed Authorization   → 401, backend NOT reached
 *   - wrong/unknown key         → 401, backend NOT reached
 *   - valid config key          → 200, backend reached, gatewayAuth populated
 *   - valid DB key (dbLookup)   → 200, backend reached
 *   - 401 body does not leak which step failed
 */
import { afterEach, describe, expect, it, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { createHash } from 'node:crypto';
import { buildAuthMiddleware } from '../gateway/auth.js';
import type { GatewayVirtualKey } from '../gateway/config.js';

const VALID_CONFIG_KEY = 'sk-aao-valid-config-key';
const VALID_DB_KEY = 'sk-aao-valid-db-key';

/** Same hashing the middleware uses for dbLookup (sha256 hex of the bearer). */
function sha256hex(s: string): string {
  return createHash('sha256').update(s).digest('hex');
}

/**
 * Bare app: [json] → [gateway auth middleware] → [sentinel backend].
 * The sentinel is the stand-in for the proxied LLM backend; if auth lets a
 * request through, `backend.mock.calls.length` becomes > 0.
 */
function makeGatewayApp(opts: {
  keys?: GatewayVirtualKey[];
  dbLookup?: (keyHash: string) => { id: string; team: string; allowedModels?: string[] | null } | null;
}) {
  const backend = vi.fn((_req: express.Request, res: express.Response) => {
    res.status(200).json({ ok: true, served: 'fake-backend' });
  });
  const app = express();
  app.use(express.json());
  app.use(buildAuthMiddleware({ keys: opts.keys ?? [], dbLookup: opts.dbLookup }));
  // Stand-in for the proxied LLM endpoint.
  app.post('/v1/chat/completions', backend);
  return { app, backend };
}

const configKeys: GatewayVirtualKey[] = [
  { key: VALID_CONFIG_KEY, team: 'team-a' },
];

describe('APIS-052 gateway proxy auth boundary', () => {
  afterEach(() => vi.restoreAllMocks());

  it('missing Authorization header → 401 and the backend is never reached', async () => {
    const { app, backend } = makeGatewayApp({ keys: configKeys });
    const res = await request(app)
      .post('/v1/chat/completions')
      .send({ model: 'auto', messages: [] });
    expect(res.status).toBe(401);
    expect(res.body).toEqual({ error: 'invalid api key' });
    expect(backend).not.toHaveBeenCalled();
  });

  it('malformed Authorization (non-Bearer scheme) → 401, no passthrough', async () => {
    const { app, backend } = makeGatewayApp({ keys: configKeys });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', `Basic ${VALID_CONFIG_KEY}`)
      .send({ model: 'auto' });
    expect(res.status).toBe(401);
    expect(backend).not.toHaveBeenCalled();
  });

  it('wrong/unknown Bearer key → 401, no passthrough', async () => {
    const { app, backend } = makeGatewayApp({ keys: configKeys });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-totally-wrong')
      .send({ model: 'auto' });
    expect(res.status).toBe(401);
    expect(res.body).toEqual({ error: 'invalid api key' });
    expect(backend).not.toHaveBeenCalled();
  });

  it('valid config key → 200, backend reached', async () => {
    const { app, backend } = makeGatewayApp({ keys: configKeys });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', `Bearer ${VALID_CONFIG_KEY}`)
      .send({ model: 'auto' });
    expect(res.status).toBe(200);
    expect(res.body.served).toBe('fake-backend');
    expect(backend).toHaveBeenCalledTimes(1);
  });

  it('valid DB key (via dbLookup) → 200, backend reached', async () => {
    const dbLookup = (hash: string) =>
      hash === sha256hex(VALID_DB_KEY) ? { id: 'k1', team: 'team-db', allowedModels: null } : null;
    const { app, backend } = makeGatewayApp({ keys: [], dbLookup });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', `Bearer ${VALID_DB_KEY}`)
      .send({ model: 'auto' });
    expect(res.status).toBe(200);
    expect(backend).toHaveBeenCalledTimes(1);
  });

  it('a key revoked from the DB (dbLookup miss) and absent from config → 401', async () => {
    // Simulate post-revocation: dbLookup returns null for everything, no config keys.
    const dbLookup = () => null;
    const { app, backend } = makeGatewayApp({ keys: [], dbLookup });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', `Bearer ${VALID_DB_KEY}`)
      .send({ model: 'auto' });
    expect(res.status).toBe(401);
    expect(backend).not.toHaveBeenCalled();
  });

  it('401 body is identical for missing vs wrong key (no information leak)', async () => {
    const { app } = makeGatewayApp({ keys: configKeys });
    const missing = await request(app).post('/v1/chat/completions').send({});
    const wrong = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-nope')
      .send({});
    expect(missing.body).toEqual(wrong.body);
    expect(missing.status).toBe(wrong.status);
  });
});
