/**
 * Capacity autoscaling signal (§5) — pure recommender unit tests + a route test.
 */

import Fastify, { type FastifyInstance } from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
import { _resetDatastoreProvider, setProvider } from '../../lib/datastore.js';
import {
  recommendScaling,
  recommendScalingStateful,
  applyHysteresis,
  scalingThresholdsFromEnv,
  cooldownMsFromEnv,
  _resetScaleStates,
  DEFAULT_SCALING_THRESHOLDS,
  DEFAULT_COOLDOWN_MS,
  type ScalingRecommendation,
  type ScaleState,
} from './autoscaler.js';
import type { FleetMetrics, FleetAlert } from './coordinator.js';

vi.mock('../../lib/auth.js', () => ({
  extractAuth: vi.fn(async () => ({ sub: 'user_1', role: 'admin' })),
}));
vi.mock('../../lib/request-context.js', () => ({
  getRequestProductId: () => 'lysnrai',
  requireProductAccess: async () => 'lysnrai',
}));

function metrics(
  over: {
    jobs?: Partial<FleetMetrics['jobs']>;
    factories?: Partial<FleetMetrics['factories']>;
    alerts?: FleetAlert[];
  } = {}
): FleetMetrics {
  return {
    productId: 'lysnrai',
    generatedAt: '2026-06-01T00:00:00.000Z',
    jobs: {
      total: 0,
      byStage: {} as FleetMetrics['jobs']['byStage'],
      queueDepth: 0,
      blocked: 0,
      active: 0,
      oldestQueuedAgeMs: null,
      ...over.jobs,
    },
    factories: {
      total: 1,
      live: 1,
      stale: 0,
      byHealth: { ok: 1, degraded: 0, down: 0 },
      seatsUsed: 0,
      seatsTotal: 4,
      utilizationPct: 0,
      ...over.factories,
    },
    budget: null,
    alerts: over.alerts ?? [],
  };
}

const alert = (code: string, level: FleetAlert['level'] = 'warning'): FleetAlert => ({
  code,
  level,
  message: code,
});

describe('recommendScaling', () => {
  it('holds when capacity is balanced against demand', () => {
    const r = recommendScaling(
      metrics({ factories: { seatsUsed: 2, seatsTotal: 4, utilizationPct: 50 } })
    );
    expect(r.action).toBe('hold');
    expect(r.delta).toBe(0);
    expect(r.recommendedSeats).toBe(4);
  });

  it('CRITICAL scale-out on no_live_capacity even from a cold (zero-seat) fleet', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 3, total: 3 },
        factories: { total: 0, live: 0, seatsUsed: 0, seatsTotal: 0, utilizationPct: 0 },
        alerts: [alert('no_live_capacity', 'critical')],
      })
    );
    expect(r.action).toBe('scale_out');
    expect(r.urgency).toBe('critical');
    expect(r.recommendedSeats).toBe(3); // min(queueDepth=3, maxStep=5)
    expect(r.delta).toBe(3);
  });

  it('scales out on saturation, bounded by maxScaleOutStep', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 20, total: 24 },
        factories: { seatsUsed: 4, seatsTotal: 4, utilizationPct: 100 },
        alerts: [alert('saturated')],
      })
    );
    expect(r.action).toBe('scale_out');
    expect(r.delta).toBe(DEFAULT_SCALING_THRESHOLDS.maxScaleOutStep); // capped at 5
    expect(r.recommendedSeats).toBe(4 + 5);
  });

  it('scales out on queue_starvation', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 1, total: 1, oldestQueuedAgeMs: 999_999 },
        factories: { seatsUsed: 1, seatsTotal: 2, utilizationPct: 50 },
        alerts: [alert('queue_starvation')],
      })
    );
    expect(r.action).toBe('scale_out');
    expect(r.delta).toBe(1);
  });

  it('scales out on high utilization with a backlog (no explicit alert)', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 2, total: 6 },
        factories: { seatsUsed: 4, seatsTotal: 4, utilizationPct: 90 },
      }),
      { scaleOutUtilizationPct: 85 }
    );
    expect(r.action).toBe('scale_out');
    expect(r.delta).toBe(2);
  });

  it('budget_exhausted suppresses scale-out (claims are blocked) → hold', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 5, total: 5 },
        factories: { seatsUsed: 4, seatsTotal: 4, utilizationPct: 100 },
        alerts: [alert('saturated'), alert('budget_exhausted', 'critical')],
      })
    );
    expect(r.action).toBe('hold');
    expect(r.reason).toMatch(/budget/i);
  });

  it('scales in idle slack down to in-use seats / floor', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 0, active: 0, total: 0 },
        factories: { seatsUsed: 1, seatsTotal: 6, utilizationPct: 16 },
      })
    );
    expect(r.action).toBe('scale_in');
    expect(r.recommendedSeats).toBe(1); // max(minSeats=0, seatsUsed=1)
    expect(r.delta).toBe(-5);
  });

  it('does NOT scale in while work is in flight', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 0, active: 2, total: 2 },
        factories: { seatsUsed: 2, seatsTotal: 6, utilizationPct: 33 },
      })
    );
    expect(r.action).toBe('hold');
  });

  it('respects a minSeats floor on scale-in', () => {
    const r = recommendScaling(
      metrics({
        jobs: { queueDepth: 0, active: 0, total: 0 },
        factories: { seatsUsed: 0, seatsTotal: 6, utilizationPct: 0 },
      }),
      { minSeats: 2 }
    );
    expect(r.action).toBe('scale_in');
    expect(r.recommendedSeats).toBe(2);
  });
});

describe('scalingThresholdsFromEnv', () => {
  const KEYS = [
    'FLEET_AUTOSCALE_SCALE_OUT_PCT',
    'FLEET_AUTOSCALE_SCALE_IN_PCT',
    'FLEET_AUTOSCALE_MAX_STEP',
    'FLEET_AUTOSCALE_MIN_SEATS',
  ];
  afterEach(() => KEYS.forEach(k => delete process.env[k]));

  it('returns an empty override set when nothing is configured', () => {
    expect(scalingThresholdsFromEnv()).toEqual({});
  });

  it('parses configured overrides and ignores invalid values', () => {
    process.env.FLEET_AUTOSCALE_SCALE_OUT_PCT = '70';
    process.env.FLEET_AUTOSCALE_MAX_STEP = '10';
    process.env.FLEET_AUTOSCALE_MIN_SEATS = 'not-a-number';
    expect(scalingThresholdsFromEnv()).toEqual({ scaleOutUtilizationPct: 70, maxScaleOutStep: 10 });
  });
});

function rec(over: Partial<ScalingRecommendation> = {}): ScalingRecommendation {
  return {
    productId: 'lysnrai',
    action: 'scale_out',
    recommendedSeats: 5,
    delta: 1,
    reason: 'x',
    urgency: 'warning',
    signals: {
      queueDepth: 1,
      blocked: 0,
      active: 0,
      seatsUsed: 4,
      seatsTotal: 4,
      utilizationPct: 100,
      pressure: 1,
      alertCodes: [],
    },
    generatedAt: '2026-06-01T00:00:00.000Z',
    ...over,
  };
}

describe('applyHysteresis', () => {
  const cfg = { cooldownMs: 1000 };

  it('emits the first action and anchors the cooldown', () => {
    const { recommendation, nextState } = applyHysteresis(
      rec({ action: 'scale_out' }),
      undefined,
      100,
      cfg
    );
    expect(recommendation.action).toBe('scale_out');
    expect(nextState).toEqual({ action: 'scale_out', at: 100 });
  });

  it('suppresses a direction reversal within the cooldown (scale_in after scale_out)', () => {
    const prior: ScaleState = { action: 'scale_out', at: 0 };
    const { recommendation, nextState } = applyHysteresis(
      rec({ action: 'scale_in', urgency: 'none', delta: -3 }),
      prior,
      500,
      cfg
    );
    expect(recommendation.action).toBe('hold');
    expect(recommendation.suppressed).toBe(true);
    expect(recommendation.delta).toBe(0);
    expect(recommendation.cooldownRemainingMs).toBe(500);
    // The anchor is preserved so the cooldown keeps elapsing.
    expect(nextState).toBe(prior);
  });

  it('allows the reversal once the cooldown has elapsed', () => {
    const prior: ScaleState = { action: 'scale_out', at: 0 };
    const { recommendation, nextState } = applyHysteresis(
      rec({ action: 'scale_in', urgency: 'none' }),
      prior,
      1500,
      cfg
    );
    expect(recommendation.action).toBe('scale_in');
    expect(nextState).toEqual({ action: 'scale_in', at: 1500 });
  });

  it('NEVER debounces a critical scale-out (capacity emergency bypasses cooldown)', () => {
    const prior: ScaleState = { action: 'scale_in', at: 0 };
    const { recommendation } = applyHysteresis(
      rec({ action: 'scale_out', urgency: 'critical' }),
      prior,
      100,
      cfg
    );
    expect(recommendation.action).toBe('scale_out');
    expect(recommendation.suppressed).toBeUndefined();
  });

  it('a hold never anchors the cooldown nor gets suppressed', () => {
    const prior: ScaleState = { action: 'scale_out', at: 0 };
    const { recommendation, nextState } = applyHysteresis(rec({ action: 'hold' }), prior, 100, cfg);
    expect(recommendation.action).toBe('hold');
    expect(nextState).toBe(prior);
  });

  it('does not suppress a same-direction repeat (keeps scaling out while saturated)', () => {
    const prior: ScaleState = { action: 'scale_out', at: 0 };
    const { recommendation, nextState } = applyHysteresis(
      rec({ action: 'scale_out' }),
      prior,
      200,
      cfg
    );
    expect(recommendation.action).toBe('scale_out');
    expect(nextState).toEqual({ action: 'scale_out', at: 200 });
  });
});

describe('recommendScalingStateful (in-memory cooldown)', () => {
  afterEach(() => _resetScaleStates());

  const saturated = () =>
    metrics({
      jobs: { queueDepth: 3, total: 3 },
      factories: { seatsUsed: 4, seatsTotal: 4, utilizationPct: 100 },
      alerts: [alert('saturated')],
    });
  const idle = () =>
    metrics({
      jobs: { queueDepth: 0, active: 0, total: 0 },
      factories: { seatsUsed: 0, seatsTotal: 6, utilizationPct: 0 },
    });

  it('debounces a scale_out → scale_in flip, then allows it after the cooldown', () => {
    const opts = { cooldownMs: 10_000 };
    const out = recommendScalingStateful(saturated(), {}, { nowMs: 0, ...opts });
    expect(out.action).toBe('scale_out');

    const flip = recommendScalingStateful(idle(), {}, { nowMs: 1_000, ...opts });
    expect(flip.action).toBe('hold');
    expect(flip.suppressed).toBe(true);

    const later = recommendScalingStateful(idle(), {}, { nowMs: 20_000, ...opts });
    expect(later.action).toBe('scale_in');
  });

  it('isolates cooldown state per product', () => {
    recommendScalingStateful(saturated(), {}, { nowMs: 0, cooldownMs: 10_000 });
    const other = recommendScalingStateful(
      { ...idle(), productId: 'flowmonk' },
      {},
      { nowMs: 1_000, cooldownMs: 10_000 }
    );
    expect(other.action).toBe('scale_in'); // different product → no cooldown carryover
  });
});

describe('cooldownMsFromEnv', () => {
  afterEach(() => delete process.env.FLEET_AUTOSCALE_COOLDOWN_SEC);
  it('defaults when unset and reads seconds when configured', () => {
    expect(cooldownMsFromEnv()).toBe(DEFAULT_COOLDOWN_MS);
    process.env.FLEET_AUTOSCALE_COOLDOWN_SEC = '60';
    expect(cooldownMsFromEnv()).toBe(60_000);
  });
});

async function buildApp(): Promise<FastifyInstance> {
  const { fleetRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  await app.register(fleetRoutes, { prefix: '/api' });
  return app;
}

describe('GET /fleet/autoscale (route)', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    vi.clearAllMocks();
  });

  it('returns hold for an empty fleet and scale_out once jobs queue with no capacity', async () => {
    const app = await buildApp();

    const idle = await app.inject({ method: 'GET', url: '/api/fleet/autoscale' });
    expect(idle.statusCode).toBe(200);
    expect(JSON.parse(idle.body).action).toBe('hold');

    // Queue a job with no factories registered → no_live_capacity → scale_out.
    await app.inject({
      method: 'POST',
      url: '/api/fleet/jobs',
      payload: { idempotencyKey: 'as-1', bodyMd: '# task' },
    });
    const hot = await app.inject({ method: 'GET', url: '/api/fleet/autoscale' });
    const body = JSON.parse(hot.body);
    expect(body.action).toBe('scale_out');
    expect(body.urgency).toBe('critical');
    expect(body.signals.queueDepth).toBe(1);
    expect(body.delta).toBeGreaterThanOrEqual(1);
  });
});
