/**
 * Assembly-level tests for createGatewayApp: route mounting, middleware
 * order (auth → allowlist → postAuth → handler), and error shapes.
 * /metrics specifics live in server.metrics-endpoint.test.ts.
 */
import { describe, it, expect, vi } from 'vitest';
import request from 'supertest';
import type { RequestHandler } from 'express';
import { createGatewayApp } from './server.js';
import type { GatewayConfig } from './config.js';
import type { BackendStatusRegistry } from '../engine/backend-status-registry.js';

function makeRegistry(): BackendStatusRegistry {
  return {
    start: () => {},
    stop: async () => {},
    getAll: () => [],
    getByNodeId: () => null,
    subscribe: () => () => {},
    refresh: async () => {},
  } as unknown as BackendStatusRegistry;
}

function makeConfig(): GatewayConfig {
  return {
    enabled: true,
    listenPort: 4000,
    requestTimeoutSec: 600,
    upstreamTimeoutSec: 30,
    shutdownGracefulSec: 30,
    backends: [{ id: 'gpu-a', endpoint: 'http://upstream', model: 'qwen3:8b', maxSlots: 1 }],
    virtualKeys: [{ key: 'sk-aao-test', team: 'alpha' }],
  };
}

function makeApp(extra: Parameters<typeof createGatewayApp>[0] extends infer D ? Partial<D> : never = {}) {
  return createGatewayApp({
    config: makeConfig(),
    registry: makeRegistry(),
    ...extra,
  });
}

describe('createGatewayApp wiring', () => {
  it('serves /health/liveness with no auth', async () => {
    const res = await request(makeApp().app).get('/health/liveness');
    expect(res.status).toBe(200);
  });

  it('serves /health with no auth', async () => {
    const res = await request(makeApp().app).get('/health');
    expect(res.status).toBe(200);
  });

  it('does not leak the X-Powered-By header', async () => {
    const res = await request(makeApp().app).get('/health');
    expect(res.headers['x-powered-by']).toBeUndefined();
  });

  it('rejects /v1/models without a virtual key', async () => {
    const res = await request(makeApp().app).get('/v1/models');
    expect(res.status).toBe(401);
  });

  it('serves /v1/models with a valid key', async () => {
    const res = await request(makeApp().app)
      .get('/v1/models')
      .set('Authorization', 'Bearer sk-aao-test');
    expect(res.status).toBe(200);
    // /v1/models exposes backend ids, not raw model names.
    expect(res.body.data.map((m: { id: string }) => m.id)).toContain('gpu-a');
  });

  it('rejects unauthenticated chat completions before reading the model', async () => {
    const res = await request(makeApp().app)
      .post('/v1/chat/completions')
      .send({ model: 'qwen3:8b', messages: [] });
    expect(res.status).toBe(401);
  });

  it('returns 503 for a model no backend serves (unrestricted key)', async () => {
    const res = await request(makeApp().app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-test')
      .send({ model: 'gpt-4o', messages: [] });
    expect(res.status).toBe(503);
  });

  it('rejects a model outside the key allowlist with 403', async () => {
    const config = makeConfig();
    config.virtualKeys = [{ key: 'sk-aao-limited', team: 'beta', allowedModels: ['other-model'] }];
    const { app } = createGatewayApp({ config, registry: makeRegistry() });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-limited')
      .send({ model: 'qwen3:8b', messages: [] });
    expect(res.status).toBe(403);
    expect(res.body.error).toContain('not allowed');
  });

  it('runs postAuthMiddleware after auth+allowlist and can short-circuit', async () => {
    const seen: string[] = [];
    const budgetReject: RequestHandler = (_req, res) => {
      seen.push('budget');
      res.status(429).json({ error: 'budget exhausted' });
    };
    const { app } = makeApp({ postAuthMiddleware: [budgetReject] });

    // Unauthenticated → 401 from auth; budget middleware never runs.
    const unauth = await request(app)
      .post('/v1/chat/completions')
      .send({ model: 'qwen3:8b', messages: [] });
    expect(unauth.status).toBe(401);
    expect(seen).toEqual([]);

    // Authenticated + allowed model → budget middleware fires.
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-test')
      .send({ model: 'qwen3:8b', messages: [] });
    expect(res.status).toBe(429);
    expect(seen).toEqual(['budget']);
  });

  it('proxies an authorized chat completion through fetchImpl', async () => {
    const fetchImpl = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ id: 'cmpl-1', choices: [] }), {
        status: 200,
        headers: { 'content-type': 'application/json' },
      }),
    );
    const { app } = makeApp({ fetchImpl: fetchImpl as unknown as typeof fetch });
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer sk-aao-test')
      .send({ model: 'qwen3:8b', messages: [{ role: 'user', content: 'hi' }], stream: false });
    expect(res.status).toBe(200);
    expect(fetchImpl).toHaveBeenCalled();
    expect(String(fetchImpl.mock.calls[0]?.[0])).toContain('http://upstream');
  });

  it('returns a JSON-shaped 404 for unknown routes', async () => {
    const res = await request(makeApp().app).get('/nope');
    expect(res.status).toBe(404);
    expect(res.body.error).toContain('GET /nope');
  });

  it('returns the JSON 404 shape for unknown methods on known paths', async () => {
    const res = await request(makeApp().app).delete('/v1/models');
    expect(res.status).toBe(404);
    expect(res.body.error).toContain('DELETE');
  });
});
