/**
 * Gateway config schema + validation tests.
 *
 * Targets the rejection rules that matter most operationally:
 *  - duplicate ids would silently mask second entry on lookup
 *  - duplicate virtual keys would never match for the second team
 *  - bad URL endpoint surfaces at startup, not at first request
 *  - missing model / non-positive slots leak through least-busy math
 */
import { describe, it, expect } from 'vitest';
import {
  normalizeGatewayConfig,
  validateGatewayConfig,
  readGatewayConfig,
  DEFAULT_GATEWAY_LISTEN_PORT,
  type GatewayConfig,
} from './config.js';
import type { AppConfig } from '../config.js';

function makeValidConfig(): GatewayConfig {
  return {
    enabled: true,
    listenPort: 4000,
    requestTimeoutSec: 600,
    upstreamTimeoutSec: 30,
    shutdownGracefulSec: 30,
    backends: [
      { id: 'gpu-a', endpoint: 'http://example.com/v1', model: 'qwen3:8b', maxSlots: 4 },
    ],
    virtualKeys: [
      { key: 'sk-team-alpha-xxx', team: 'alpha' },
    ],
  };
}

describe('normalizeGatewayConfig', () => {
  it('fills missing fields with documented defaults', () => {
    const norm = normalizeGatewayConfig(undefined);
    expect(norm.enabled).toBe(false);
    expect(norm.listenPort).toBe(DEFAULT_GATEWAY_LISTEN_PORT);
    expect(norm.requestTimeoutSec).toBe(600);
    expect(norm.upstreamTimeoutSec).toBe(30);
    expect(norm.backends).toEqual([]);
    expect(norm.virtualKeys).toEqual([]);
    // Phase 3a F8: shutdownGracefulSec defaults to 30s.
    expect(norm.shutdownGracefulSec).toBe(30);
  });

  it('preserves a custom shutdownGracefulSec when valid (F8)', () => {
    const norm = normalizeGatewayConfig({ shutdownGracefulSec: 90 });
    expect(norm.shutdownGracefulSec).toBe(90);
  });

  it('falls back to default when shutdownGracefulSec is non-positive (F8)', () => {
    expect(normalizeGatewayConfig({ shutdownGracefulSec: 0 }).shutdownGracefulSec).toBe(30);
    expect(normalizeGatewayConfig({ shutdownGracefulSec: -10 }).shutdownGracefulSec).toBe(30);
    expect(normalizeGatewayConfig({ shutdownGracefulSec: Number.NaN }).shutdownGracefulSec).toBe(30);
  });

  it('leaves invalid max_slots as NaN so validate can reject it loudly', () => {
    // Previously this coerced to 1 silently — a typo like `max_slot: 4`
    // (missing the `s`) would throttle a 4-slot backend to 1 forever.
    // We now normalize invalid input to NaN and rely on validate to
    // refuse boot.
    const norm = normalizeGatewayConfig({
      backends: [{ id: 'x', endpoint: 'http://x', model: 'm', maxSlots: 0 }],
    });
    expect(Number.isNaN(norm.backends[0]!.maxSlots)).toBe(true);
  });

  it('treats enabled !== true as false (typo-proof)', () => {
    const norm = normalizeGatewayConfig({ enabled: 1 as unknown as boolean });
    expect(norm.enabled).toBe(false);
  });
});

describe('validateGatewayConfig', () => {
  it('accepts a minimal valid config', () => {
    expect(validateGatewayConfig(makeValidConfig())).toEqual([]);
  });

  it('rejects empty backends array', () => {
    const c = makeValidConfig();
    c.backends = [];
    const errs = validateGatewayConfig(c);
    expect(errs.length).toBeGreaterThan(0);
    expect(errs[0]).toMatch(/backends.*non-empty/);
  });

  it('flags duplicate backend ids', () => {
    const c = makeValidConfig();
    c.backends = [
      { id: 'gpu-a', endpoint: 'http://x/v1', model: 'm', maxSlots: 1 },
      { id: 'gpu-a', endpoint: 'http://y/v1', model: 'm', maxSlots: 1 },
    ];
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /duplicate id 'gpu-a'/.test(e))).toBe(true);
  });

  it('flags invalid backend endpoint URL', () => {
    const c = makeValidConfig();
    c.backends[0]!.endpoint = 'not a url';
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /not a valid URL/.test(e))).toBe(true);
  });

  it('rejects file:// scheme on backend endpoint', () => {
    const c = makeValidConfig();
    c.backends[0]!.endpoint = 'file:///etc/passwd';
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /must use http: or https:/.test(e))).toBe(true);
  });

  it('rejects data: scheme on backend endpoint', () => {
    const c = makeValidConfig();
    c.backends[0]!.endpoint = 'data:text/plain,xxx';
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /must use http: or https:/.test(e))).toBe(true);
  });

  it('rejects gopher:// scheme on backend endpoint', () => {
    const c = makeValidConfig();
    c.backends[0]!.endpoint = 'gopher://x:8080/';
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /must use http: or https:/.test(e))).toBe(true);
  });

  it('rejects max_slots when undefined (typo guard for `max_slot:` etc.)', () => {
    // A typo in the YAML key (missing the trailing `s`) means
    // normalizeBackend sees undefined. Without the explicit check we'd
    // run with maxSlots=1 silently.
    const norm = normalizeGatewayConfig({
      backends: [
        // @ts-expect-error — intentionally omit maxSlots to exercise the guard.
        { id: 'x', endpoint: 'http://x/v1', model: 'm' },
      ],
    });
    const errs = validateGatewayConfig(norm);
    expect(errs.some(e => /max_slots/.test(e))).toBe(true);
  });

  it('rejects max_slots = 0', () => {
    const norm = normalizeGatewayConfig({
      backends: [{ id: 'x', endpoint: 'http://x/v1', model: 'm', maxSlots: 0 }],
    });
    const errs = validateGatewayConfig(norm);
    expect(errs.some(e => /max_slots/.test(e))).toBe(true);
  });

  it('rejects negative max_slots', () => {
    const norm = normalizeGatewayConfig({
      backends: [{ id: 'x', endpoint: 'http://x/v1', model: 'm', maxSlots: -1 }],
    });
    const errs = validateGatewayConfig(norm);
    expect(errs.some(e => /max_slots/.test(e))).toBe(true);
  });

  it('accepts a positive integer max_slots (regression)', () => {
    const norm = normalizeGatewayConfig({
      backends: [{ id: 'x', endpoint: 'http://x/v1', model: 'm', maxSlots: 4 }],
    });
    const errs = validateGatewayConfig(norm);
    expect(errs.filter(e => /max_slots/.test(e))).toEqual([]);
  });

  it('accepts http: and https: schemes on backend endpoint (regression)', () => {
    for (const ep of ['http://x:8080/v1', 'https://gpu.example.com/v1']) {
      const c = makeValidConfig();
      c.backends[0]!.endpoint = ep;
      const errs = validateGatewayConfig(c);
      expect(errs.filter(e => /endpoint/.test(e))).toEqual([]);
    }
  });

  it('flags duplicate virtual keys', () => {
    const c = makeValidConfig();
    c.virtualKeys = [
      { key: 'sk-dup', team: 'a' },
      { key: 'sk-dup', team: 'b' },
    ];
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /duplicate key/.test(e))).toBe(true);
  });

  it('rejects out-of-range listen_port', () => {
    const c = makeValidConfig();
    c.listenPort = 70000;
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /listen_port/.test(e))).toBe(true);
  });

  it('rejects non-array allowed_models when defined', () => {
    const c = makeValidConfig();
    c.virtualKeys[0]!.allowedModels = 'qwen' as unknown as string[];
    const errs = validateGatewayConfig(c);
    expect(errs.some(e => /allowed_models/.test(e))).toBe(true);
  });

  describe('metrics.prefix length cap (Phase 3b post-review)', () => {
    it('rejects 1-character prefix (length under 2)', () => {
      const c = makeValidConfig();
      c.metrics = { enabled: true, prefix: 'a' };
      const errs = validateGatewayConfig(c);
      expect(errs.some(e => /prefix length must be 2-64/.test(e))).toBe(true);
    });

    it('rejects 65-character prefix (length over 64)', () => {
      const c = makeValidConfig();
      c.metrics = { enabled: true, prefix: 'a'.repeat(65) };
      const errs = validateGatewayConfig(c);
      expect(errs.some(e => /prefix length must be 2-64/.test(e))).toBe(true);
    });

    it('accepts 64-character prefix', () => {
      const c = makeValidConfig();
      c.metrics = { enabled: true, prefix: 'a'.repeat(64) };
      const errs = validateGatewayConfig(c);
      expect(errs.filter(e => /prefix length/.test(e))).toEqual([]);
    });

    it('accepts 2-character prefix', () => {
      const c = makeValidConfig();
      c.metrics = { enabled: true, prefix: 'ab' };
      const errs = validateGatewayConfig(c);
      expect(errs.filter(e => /prefix length/.test(e))).toEqual([]);
    });

    it('still rejects invalid prefix shape (regex check runs first)', () => {
      const c = makeValidConfig();
      c.metrics = { enabled: true, prefix: '0starts_with_digit' };
      const errs = validateGatewayConfig(c);
      expect(errs.some(e => /prefix must match/.test(e))).toBe(true);
    });
  });
});

describe('readGatewayConfig', () => {
  it('returns a normalized config even when AppConfig has no gateway slice', () => {
    const app: Partial<AppConfig> = { worktreeDir: '/tmp' };
    const got = readGatewayConfig(app as AppConfig);
    expect(got.enabled).toBe(false);
    expect(got.listenPort).toBe(DEFAULT_GATEWAY_LISTEN_PORT);
  });

  it('reads enabled / listen_port from AppConfig', () => {
    const app: Record<string, unknown> = {
      worktreeDir: '/tmp',
      gateway: { enabled: true, listenPort: 5000, backends: [] },
    };
    const got = readGatewayConfig(app as unknown as AppConfig);
    expect(got.enabled).toBe(true);
    expect(got.listenPort).toBe(5000);
  });
});
