/**
 * Health endpoint conversion tests.
 *
 * Verifies that:
 *  - online backends land in healthy_endpoints
 *  - offline backends land in unhealthy_endpoints with their probe error
 *  - missing probe rows produce a synthetic "probe pending" entry
 *  - counts match array lengths
 */
import { describe, it, expect } from 'vitest';
import { buildHealthBody, buildLivenessHandler } from './health-endpoint.js';
import type { GatewayBackendConfig } from './config.js';
import type { NodeStatus } from '../engine/backend-status-registry.js';

function bk(id: string): GatewayBackendConfig {
  return { id, endpoint: `http://${id}/v1`, model: 'm', maxSlots: 1 };
}

function st(id: string, online: boolean, err?: string): NodeStatus {
  return {
    nodeId: id,
    workerId: id,
    source: 'direct',
    online,
    busy: false,
    busySlots: 0,
    totalSlots: 1,
    loadedModel: null,
    throughputTps: null,
    lastSeen: new Date().toISOString(),
    ...(err ? { lastProbeError: err } : {}),
  };
}

describe('buildHealthBody', () => {
  it('emits empty arrays when no backends', () => {
    const body = buildHealthBody([]);
    expect(body.healthy_endpoints).toEqual([]);
    expect(body.unhealthy_endpoints).toEqual([]);
    expect(body.healthy_count).toBe(0);
    expect(body.unhealthy_count).toBe(0);
  });

  it('classifies online into healthy and attaches AAO busy snapshot', () => {
    const body = buildHealthBody([
      { config: bk('a'), status: st('a', true) },
    ]);
    expect(body.healthy_endpoints).toHaveLength(1);
    const entry = body.healthy_endpoints[0]!;
    // LiteLLM-shape contract still intact.
    expect(entry.model).toBe('a');
    expect(entry.litellm_params).toEqual({ model: 'a' });
    // AAO extension fields populated so every client AAO that probes
    // this endpoint inherits the gateway-aggregated busy view.
    expect(entry.aao_busy_slots).toBe(0);
    expect(entry.aao_total_slots).toBe(1);
    expect(entry.aao_saturated).toBe(false);
    expect(entry.aao_last_seen).toEqual(expect.any(String));
    expect(body.unhealthy_count).toBe(0);
  });

  it('takes max(probedBusy, liveInflight) so sub-probe-cadence chats are visible', () => {
    // Regression for the 2026-05-21 follow-up: probes run on a 5–30s
    // cadence; a chat that starts and ends within a single cycle was
    // invisible to /health. The live in-flight counter (incremented
    // by stream-proxy on dispatch) now overrides the probe value when
    // it's higher.
    const inflight = {
      get: (id: string) => (id === 'a' ? 2 : 0),
      inc: () => undefined,
      dec: () => undefined,
      snapshot: () => new Map(),
    };
    const body = buildHealthBody([
      { config: bk('a'), status: st('a', true) }, // probed busy=0
      { config: bk('b'), status: st('b', true) },
    ], inflight);
    const a = body.healthy_endpoints.find(e => e.model === 'a')!;
    const b = body.healthy_endpoints.find(e => e.model === 'b')!;
    expect(a.aao_busy_slots).toBe(2); // inflight wins
    expect(b.aao_busy_slots).toBe(0); // no inflight, falls back to probe
  });

  it('flags aao_saturated when busy >= total', () => {
    const status: NodeStatus = {
      ...st('a', true),
      busySlots: 4,
      totalSlots: 4,
      busy: true,
    };
    const body = buildHealthBody([
      { config: bk('a'), status },
    ]);
    const entry = body.healthy_endpoints[0]!;
    expect(entry.aao_busy_slots).toBe(4);
    expect(entry.aao_total_slots).toBe(4);
    expect(entry.aao_saturated).toBe(true);
  });

  it('classifies offline into unhealthy with probe error', () => {
    const body = buildHealthBody([
      { config: bk('a'), status: st('a', false, 'connection refused') },
    ]);
    expect(body.unhealthy_endpoints[0]!.error).toBe('connection refused');
    expect(body.healthy_count).toBe(0);
  });

  it('omits unprobed backends so first-boot does not flash all-red', () => {
    // LiteLLM-compat: unprobed backends are absent from both arrays.
    // Without this, NodeStatusWidget would paint every backend red for
    // the ~5s between bind and the first probe cycle landing.
    const body = buildHealthBody([
      { config: bk('a'), status: null },
    ]);
    expect(body.healthy_endpoints).toEqual([]);
    expect(body.unhealthy_endpoints).toEqual([]);
    expect(body.healthy_count).toBe(0);
    expect(body.unhealthy_count).toBe(0);
  });

  it('mixes probed-online with unprobed correctly (only probed appears)', () => {
    const body = buildHealthBody([
      { config: bk('a'), status: st('a', true) },
      { config: bk('b'), status: null },
      { config: bk('c'), status: st('c', false, 'connection refused') },
    ]);
    // 'a' healthy, 'c' unhealthy, 'b' missing from both.
    expect(body.healthy_endpoints.map(e => e.model)).toEqual(['a']);
    expect(body.unhealthy_endpoints.map(e => e.model)).toEqual(['c']);
    expect(body.healthy_count).toBe(1);
    expect(body.unhealthy_count).toBe(1);
  });

  it('counts match array lengths', () => {
    const body = buildHealthBody([
      { config: bk('a'), status: st('a', true) },
      { config: bk('b'), status: st('b', false, 'x') },
      { config: bk('c'), status: st('c', true) },
    ]);
    expect(body.healthy_count).toBe(2);
    expect(body.unhealthy_count).toBe(1);
  });
});

describe('buildLivenessHandler', () => {
  it('returns 200 with status ok', () => {
    const h = buildLivenessHandler();
    let code: number | undefined;
    let body: unknown;
    const res = {
      status: (c: number) => { code = c; return res; },
      json: (b: unknown) => { body = b; return res; },
    };
    h({} as never, res as never);
    expect(code).toBe(200);
    expect(body).toEqual({ status: 'ok' });
  });
});
