import { describe, it, expect } from 'vitest';
import { cosineSimilarity, normalizeVector, topKByCosine } from '../cosine.js';

describe('cosineSimilarity', () => {
  it('returns 1.0 for identical vectors', () => {
    expect(cosineSimilarity([1, 0, 0], [1, 0, 0])).toBeCloseTo(1.0);
  });

  it('returns 0.0 for orthogonal vectors', () => {
    expect(cosineSimilarity([1, 0], [0, 1])).toBeCloseTo(0.0);
  });

  it('returns -1.0 for opposite vectors', () => {
    expect(cosineSimilarity([1, 0], [-1, 0])).toBeCloseTo(-1.0);
  });

  it('returns 0 for empty vectors', () => {
    expect(cosineSimilarity([], [])).toBe(0);
  });

  it('returns 0 for mismatched dimensions', () => {
    expect(cosineSimilarity([1, 2], [1, 2, 3])).toBe(0);
  });

  it('returns 0 for zero vector', () => {
    expect(cosineSimilarity([0, 0, 0], [1, 2, 3])).toBe(0);
  });

  it('computes correct similarity for arbitrary vectors', () => {
    const a = [1, 2, 3];
    const b = [4, 5, 6];
    // Known value: (4+10+18) / (sqrt(14)*sqrt(77)) ≈ 0.9746
    expect(cosineSimilarity(a, b)).toBeCloseTo(0.9746, 3);
  });
});

describe('normalizeVector', () => {
  it('normalizes to unit length', () => {
    const v = normalizeVector([3, 4]);
    const magnitude = Math.sqrt(v[0] ** 2 + v[1] ** 2);
    expect(magnitude).toBeCloseTo(1.0);
  });

  it('returns zero vector for zero input', () => {
    expect(normalizeVector([0, 0, 0])).toEqual([0, 0, 0]);
  });

  it('preserves direction', () => {
    const v = normalizeVector([2, 0, 0]);
    expect(v[0]).toBeCloseTo(1);
    expect(v[1]).toBeCloseTo(0);
    expect(v[2]).toBeCloseTo(0);
  });
});

describe('topKByCosine', () => {
  const items = [
    { id: 'a', emb: [1, 0, 0] },
    { id: 'b', emb: [0, 1, 0] },
    { id: 'c', emb: [0.9, 0.1, 0] },
    { id: 'd', emb: undefined as number[] | undefined },
  ];

  it('returns top-K sorted by score', () => {
    const results = topKByCosine([1, 0, 0], items, i => i.emb, 2);
    expect(results).toHaveLength(2);
    expect(results[0].item.id).toBe('a');
    expect(results[0].score).toBeCloseTo(1.0);
    expect(results[1].item.id).toBe('c');
  });

  it('skips items with missing embeddings', () => {
    const results = topKByCosine([1, 0, 0], items, i => i.emb, 10);
    expect(results).toHaveLength(3); // 'd' skipped
  });

  it('respects minScore filter', () => {
    const results = topKByCosine([1, 0, 0], items, i => i.emb, 10, 0.5);
    expect(results.every(r => r.score >= 0.5)).toBe(true);
  });

  it('returns empty array for empty items', () => {
    const results = topKByCosine([1, 0, 0], [], () => undefined, 5);
    expect(results).toEqual([]);
  });
});
