import { describe, it, expect } from 'vitest';
import { rrf } from './rrf';

describe('rrf', () => {
  it('returns [] for no lists / empty lists', () => {
    expect(rrf([])).toEqual([]);
    expect(rrf([[], []])).toEqual([]);
  });

  it('ranks a single list by its existing order', () => {
    const out = rrf([['a', 'b', 'c']]);
    expect(out.map((r) => r.id)).toEqual(['a', 'b', 'c']);
    // rank-1 score = 1/(60+1)
    expect(out[0]!.score).toBeCloseTo(1 / 61, 10);
    expect(out[1]!.score).toBeCloseTo(1 / 62, 10);
  });

  it('sums contributions for ids in multiple lists', () => {
    // 'a' is rank 1 in list1 and rank 1 in list2 => 2/(k+1).
    const out = rrf([
      ['a', 'b'],
      ['a', 'c'],
    ]);
    const a = out.find((r) => r.id === 'a')!;
    expect(a.score).toBeCloseTo(2 / 61, 10);
    // 'a' should rank above 'b' and 'c' (each only 1/(k+2)).
    expect(out[0]!.id).toBe('a');
  });

  it('an item ranked high in both lists beats one ranked high in only one', () => {
    const semantic = ['x', 'y', 'z'];
    const lexical = ['y', 'x', 'w'];
    const out = rrf([semantic, lexical]);
    // y: 1/(60+2) + 1/(60+1) ; x: 1/(60+1) + 1/(60+2) -> tie, but both top.
    expect(new Set([out[0]!.id, out[1]!.id])).toEqual(new Set(['x', 'y']));
    // z and w each appear once, so they rank lower.
    expect(out.slice(2).map((r) => r.id).sort()).toEqual(['w', 'z']);
  });

  it('respects a custom k', () => {
    const out = rrf([['a']], 0);
    // rank 1 with k=0 => 1/1 = 1.
    expect(out[0]!.score).toBeCloseTo(1, 10);
  });

  it('sorts by fused score descending', () => {
    const out = rrf([
      ['a', 'b', 'c'],
      ['a', 'b', 'c'],
    ]);
    expect(out.map((r) => r.id)).toEqual(['a', 'b', 'c']);
    expect(out[0]!.score).toBeGreaterThan(out[1]!.score);
    expect(out[1]!.score).toBeGreaterThan(out[2]!.score);
  });
});
