import { describe, it, expect } from 'vitest';
import { pickIdlerIndex, type ClaimCandidate } from './idle-routing.js';

const c = (freeSlots: number, opts: Partial<ClaimCandidate> = {}): ClaimCandidate => ({
  freeSlots,
  availableForClaim: opts.availableForClaim ?? true,
  servesRole: opts.servesRole ?? true,
});

describe('pickIdlerIndex (most-free-wins)', () => {
  it('returns -1 when there are no siblings', () => {
    expect(pickIdlerIndex(0, [])).toBe(-1);
  });

  it('yields to a strictly-idler sibling (0/n beats a loaded worker)', () => {
    // self has 0 free (fully loaded); sibling has 4 free.
    expect(pickIdlerIndex(0, [c(4)])).toBe(0);
  });

  it('does not yield on a tie (claims itself)', () => {
    expect(pickIdlerIndex(2, [c(2), c(2)])).toBe(-1);
  });

  it('does not yield when the caller is the most free', () => {
    expect(pickIdlerIndex(4, [c(1), c(3)])).toBe(-1);
  });

  it('picks the idlest among several stricter competitors', () => {
    expect(pickIdlerIndex(1, [c(2), c(5), c(3)])).toBe(1); // 5 free wins
  });

  it('ignores siblings that do not serve the role', () => {
    expect(pickIdlerIndex(0, [c(8, { servesRole: false })])).toBe(-1);
  });

  it('ignores unavailable (unhealthy/stopped) siblings', () => {
    expect(pickIdlerIndex(0, [c(8, { availableForClaim: false })])).toBe(-1);
  });

  it('skips a non-serving idler but still yields to a serving one', () => {
    // idx0: very idle but wrong role; idx1: idle and serves → pick idx1.
    expect(pickIdlerIndex(0, [c(9, { servesRole: false }), c(3)])).toBe(1);
  });
});
