/**
 * Phase 3c — shared-dependencies factory unit tests.
 *
 * Covers:
 *  1. start/stop lifecycle is idempotent (double-start no-op, double-stop no-op)
 *  2. start() subscribes to the BackendStatusRegistry + writes gauges
 *  3. stop() drops accumulated backend gauge labels
 *  4. stateless mode (repo=null) skips post-auth middleware + cache
 *  5. promRegistry=null skips metrics entirely
 *  6. Repository hangs __gatewayKeyCache / __gatewayMetrics on start,
 *     drops them on stop
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Registry } from 'prom-client';
import {
  createSharedGatewayDependencies,
} from './shared-dependencies.js';
import { normalizeGatewayConfig } from './config.js';
import type { BackendStatusRegistry, NodeStatus, NodeStatusListener, Unsubscribe } from '../engine/backend-status-registry.js';
import type { Repository } from '../db/repository.js';
import type { KeyCache } from './key-cache.js';
import type { GatewayMetrics } from '../metrics/gateway-metrics.js';

// Build a minimal fake registry that lets the test drive subscribe callbacks.
function fakeRegistry(): BackendStatusRegistry & {
  emit(snapshot: NodeStatus[]): void;
  listenerCount(): number;
} {
  const listeners = new Set<NodeStatusListener>();
  const reg: BackendStatusRegistry = {
    start: () => {},
    stop: async () => {},
    getAll: () => [],
    subscribe: (l: NodeStatusListener): Unsubscribe => {
      listeners.add(l);
      return () => listeners.delete(l);
    },
    noteSubscriberActivity: () => {},
  };
  return Object.assign(reg, {
    emit: (snapshot: NodeStatus[]) => {
      for (const l of listeners) l(snapshot);
    },
    listenerCount: () => listeners.size,
  });
}

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

const baseConfig = () =>
  normalizeGatewayConfig({
    enabled: true,
    backends: [
      { id: 'gpu-a', endpoint: 'http://gpu-a:8080/v1', model: 'qwen3:8b', maxSlots: 4 },
    ],
    virtualKeys: [],
  });

describe('createSharedGatewayDependencies', () => {
  let registry: ReturnType<typeof fakeRegistry>;
  let promReg: Registry;

  beforeEach(() => {
    registry = fakeRegistry();
    promReg = new Registry();
  });

  it('with no repo, stateless: no middleware / no rate limiter / no usage recorder', () => {
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: null,
    });
    expect(deps.keyCache).toBeNull();
    expect(deps.rateLimiter).toBeNull();
    expect(deps.usageRecorder).toBeNull();
    expect(deps.postAuthMiddleware).toHaveLength(0);
    expect(deps.dbLookup).toBeUndefined();
    expect(deps.touchLastUsed).toBeUndefined();
  });

  it('with promRegistry=null, metrics handle is null even with repo', () => {
    const repoShape = {
      findGatewayVirtualKeyByHash: vi.fn(() => null),
      findGatewayVirtualKeyById: vi.fn(() => null),
      touchGatewayVirtualKeyLastUsed: vi.fn(),
      getGatewayKeyUsage: vi.fn(() => null),
      incrementGatewayKeyUsage: vi.fn(),
    };
    const repo = repoShape as unknown as Repository;
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo,
      promRegistry: null,
    });
    expect(deps.metrics).toBeNull();
    // Stateless metric path: backend snapshots are no-ops (no subscribe).
    deps.start();
    expect(registry.listenerCount()).toBe(0);
  });

  it('with promRegistry, start() subscribes to the registry and writes gauges', async () => {
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: promReg,
      prefix: 'aao_test',
    });
    expect(deps.metrics).not.toBeNull();
    deps.start();
    expect(registry.listenerCount()).toBe(1);

    registry.emit([mkNode('gpu-a', 2, 4, true), mkNode('gpu-b', 0, 8, false)]);
    const out = await promReg.metrics();
    expect(out).toMatch(/aao_test_backend_busy_slots\{backend="gpu-a"\} 2/);
    expect(out).toMatch(/aao_test_backend_total_slots\{backend="gpu-b"\} 8/);
    expect(out).toMatch(/aao_test_backend_online\{backend="gpu-b"\} 0/);
  });

  it('start() is idempotent', () => {
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: promReg,
    });
    deps.start();
    deps.start();
    deps.start();
    // Subscribed exactly once
    expect(registry.listenerCount()).toBe(1);
  });

  it('stop() unsubscribes and clears backend gauges; new start() rebuilds', async () => {
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: promReg,
      prefix: 'aao_lifecycle',
    });
    deps.start();
    registry.emit([mkNode('gpu-a', 1)]);
    let out = await promReg.metrics();
    expect(out).toMatch(/aao_lifecycle_backend_busy_slots\{backend="gpu-a"\} 1/);

    await deps.stop();
    expect(registry.listenerCount()).toBe(0);
    // After stop, the per-backend gauge labels are dropped from the
    // registry so a re-start with a different set of backends doesn't
    // leak stale series.
    out = await promReg.metrics();
    expect(out).not.toMatch(/aao_lifecycle_backend_busy_slots\{backend="gpu-a"\}/);
  });

  it('stop() before start() is a no-op', async () => {
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: promReg,
    });
    await expect(deps.stop()).resolves.toBeUndefined();
  });

  it('hangs __gatewayKeyCache + __gatewayMetrics on Repository and drops them on stop', async () => {
    const repoShape = {
      findGatewayVirtualKeyByHash: vi.fn(() => null),
      findGatewayVirtualKeyById: vi.fn(() => null),
      touchGatewayVirtualKeyLastUsed: vi.fn(),
      getGatewayKeyUsage: vi.fn(() => null),
      incrementGatewayKeyUsage: vi.fn(),
    };
    const repo = repoShape as unknown as Repository;
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo,
      promRegistry: promReg,
      prefix: 'aao_hang',
    });
    expect(
      (repo as unknown as { __gatewayKeyCache?: KeyCache }).__gatewayKeyCache,
    ).toBe(deps.keyCache);
    expect(
      (repo as unknown as { __gatewayMetrics?: GatewayMetrics }).__gatewayMetrics,
    ).toBe(deps.metrics);
    deps.start();
    await deps.stop();
    expect(
      (repo as unknown as { __gatewayKeyCache?: KeyCache }).__gatewayKeyCache,
    ).toBeUndefined();
    expect(
      (repo as unknown as { __gatewayMetrics?: GatewayMetrics }).__gatewayMetrics,
    ).toBeUndefined();
  });

  it('shared Registry: 2nd createSharedGatewayDependencies + start() does NOT throw on duplicate metric registration', async () => {
    // Phase 3c CRITICAL-1 regression guard.
    //
    // Same-process gateway bounce (enable → disable → enable, or backend
    // list edit) calls `createSharedGatewayDependencies` twice against
    // the SAME Registry the bridge owns. prom-client throws if the
    // second call re-registers the same metric name on the same
    // registry — the `createGatewayMetrics` cache must short-circuit
    // before that happens.
    const sharedReg = new Registry();
    const first = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo: null,
      promRegistry: sharedReg,
      prefix: 'aao_shared',
    });
    first.start();
    registry.emit([mkNode('gpu-a', 1)]);
    await first.stop();

    // Second cycle on the SAME Registry: must not throw.
    expect(() => {
      const second = createSharedGatewayDependencies({
        config: baseConfig(),
        registry,
        repo: null,
        promRegistry: sharedReg,
        prefix: 'aao_shared',
      });
      second.start();
      registry.emit([mkNode('gpu-a', 3)]);
    }).not.toThrow();

    // Metric series re-populated after the bounce.
    const out = await sharedReg.metrics();
    expect(out).toMatch(/aao_shared_backend_busy_slots\{backend="gpu-a"\} 3/);
  });

  it('with repo, post-auth middleware contains budget + rate-limit (2 entries)', () => {
    const repoShape = {
      findGatewayVirtualKeyByHash: vi.fn(() => null),
      findGatewayVirtualKeyById: vi.fn(() => null),
      touchGatewayVirtualKeyLastUsed: vi.fn(),
      getGatewayKeyUsage: vi.fn(() => null),
      incrementGatewayKeyUsage: vi.fn(),
    };
    const repo = repoShape as unknown as Repository;
    const deps = createSharedGatewayDependencies({
      config: baseConfig(),
      registry,
      repo,
      promRegistry: null,
    });
    expect(deps.postAuthMiddleware).toHaveLength(2);
    expect(deps.rateLimiter).not.toBeNull();
    expect(deps.usageRecorder).not.toBeNull();
    expect(deps.dbLookup).toBeDefined();
    expect(deps.touchLastUsed).toBeDefined();
  });
});
