/**
 * Phase 3b — integration test that drives the metric wiring patterns
 * the bootstrap installs (BackendStatusRegistry.subscribe → gauges,
 * dbLookup → cache hit/miss). We don't boot a full HTTP listener; we
 * just verify the bridge code is shaped correctly by simulating the
 * upstream callbacks.
 */
import { describe, it, expect, beforeEach } from 'vitest';
import { Registry } from 'prom-client';
import { createGatewayMetrics } from '../metrics/gateway-metrics.js';
import type { NodeStatus } from '../engine/backend-status-registry.js';

describe('gateway bootstrap metrics wiring (subscribe → gauges)', () => {
  let reg: Registry;

  beforeEach(() => {
    reg = new Registry();
  });

  it('BackendStatusRegistry snapshot drives backend gauges', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_b');
    // Simulate what bootstrap.ts subscribes — call the same body on a
    // fake snapshot.
    const snapshot: NodeStatus[] = [
      {
        nodeId: 'gpu-a',
        workerId: 'gpu-a',
        source: 'direct',
        online: true,
        busy: true,
        busySlots: 3,
        totalSlots: 8,
        loadedModel: 'qwen3:8b',
        throughputTps: null,
        lastSeen: new Date().toISOString(),
      },
      {
        nodeId: 'gpu-b',
        workerId: 'gpu-b',
        source: 'direct',
        online: false,
        busy: false,
        busySlots: 0,
        totalSlots: 4,
        loadedModel: null,
        throughputTps: null,
        lastSeen: new Date().toISOString(),
      },
    ];
    for (const node of snapshot) {
      metrics.backendBusySlots.labels({ backend: node.nodeId }).set(node.busySlots);
      metrics.backendTotalSlots.labels({ backend: node.nodeId }).set(node.totalSlots);
      metrics.backendOnline.labels({ backend: node.nodeId }).set(node.online ? 1 : 0);
    }
    const out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_b_backend_busy_slots\{backend="gpu-a"\} 3/);
    expect(out).toMatch(/aao_gateway_b_backend_total_slots\{backend="gpu-a"\} 8/);
    expect(out).toMatch(/aao_gateway_b_backend_online\{backend="gpu-a"\} 1/);
    expect(out).toMatch(/aao_gateway_b_backend_online\{backend="gpu-b"\} 0/);
  });

  it('cacheHit/Miss counters split by cache label', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_c');
    metrics.cacheHitTotal.labels({ cache: 'key' }).inc(7);
    metrics.cacheMissTotal.labels({ cache: 'key' }).inc(2);
    const out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_c_cache_hit_total\{cache="key"\} 7/);
    expect(out).toMatch(/aao_gateway_c_cache_miss_total\{cache="key"\} 2/);
  });

  it('rate-limit + budget reject counters live under requests_total too', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_r');
    metrics.rateLimitRejectionsTotal.labels({ team: 'alpha' }).inc();
    metrics.requestsTotal
      .labels({ team: 'alpha', backend: 'none', model: 'none', status: 'rate_limited' })
      .inc();
    metrics.requestsTotal
      .labels({ team: 'beta', backend: 'none', model: 'none', status: 'budget_exhausted' })
      .inc();
    const out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_r_rate_limit_rejections_total\{team="alpha"\} 1/);
    expect(out).toMatch(/aao_gateway_r_requests_total\{[^}]*team="alpha"[^}]*status="rate_limited"[^}]*\} 1/);
    expect(out).toMatch(/aao_gateway_r_requests_total\{[^}]*team="beta"[^}]*status="budget_exhausted"[^}]*\} 1/);
  });

  it('budget_used_ratio reflects the latest set()', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_bud');
    metrics.budgetUsedRatio.labels({ team: 'alpha', key_prefix: 'abc12345' }).set(0.42);
    metrics.budgetUsedRatio.labels({ team: 'alpha', key_prefix: 'abc12345' }).set(0.88);
    const out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_bud_virtual_key_budget_used_ratio\{team="alpha",key_prefix="abc12345"\} 0\.88/);
  });

  it('active_streams gauge updates with stream count', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_as');
    metrics.activeStreams.set(3);
    let out = await reg.metrics();
    expect(out).toContain('aao_gateway_as_active_streams 3');
    metrics.activeStreams.set(0);
    out = await reg.metrics();
    expect(out).toContain('aao_gateway_as_active_streams 0');
  });

  // Phase 3b post-review: backend labels must be dropped when a backend
  // disappears from the snapshot. The bootstrap subscribe body has the
  // diff logic; we exercise the same shape inline.
  it('removes backend gauge labels when a backend disappears from the snapshot', async () => {
    const metrics = createGatewayMetrics(reg, 'aao_gateway_drop');
    let prev = new Set<string>();
    const apply = (snapshot: NodeStatus[]): void => {
      const current = new Set<string>();
      for (const node of snapshot) {
        current.add(node.nodeId);
        metrics.backendBusySlots.labels({ backend: node.nodeId }).set(node.busySlots);
        metrics.backendTotalSlots.labels({ backend: node.nodeId }).set(node.totalSlots);
        metrics.backendOnline.labels({ backend: node.nodeId }).set(node.online ? 1 : 0);
      }
      for (const goneId of prev) {
        if (current.has(goneId)) continue;
        metrics.backendBusySlots.remove({ backend: goneId });
        metrics.backendTotalSlots.remove({ backend: goneId });
        metrics.backendOnline.remove({ backend: goneId });
      }
      prev = current;
    };

    const mkNode = (id: string, online = true): NodeStatus => ({
      nodeId: id,
      workerId: id,
      source: 'direct',
      online,
      busy: false,
      busySlots: 0,
      totalSlots: 4,
      loadedModel: 'qwen3:8b',
      throughputTps: null,
      lastSeen: new Date().toISOString(),
    });

    // Snapshot 1: gpu-a + gpu-b
    apply([mkNode('gpu-a'), mkNode('gpu-b')]);
    let out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_drop_backend_online\{backend="gpu-a"\}/);
    expect(out).toMatch(/aao_gateway_drop_backend_online\{backend="gpu-b"\}/);
    // Snapshot 2: gpu-a only (gpu-b dropped)
    apply([mkNode('gpu-a')]);
    out = await reg.metrics();
    expect(out).toMatch(/aao_gateway_drop_backend_online\{backend="gpu-a"\}/);
    expect(out).not.toMatch(/backend="gpu-b"/);
  });
});
