/**
 * ADR-121 Phase 3b — `@ruvector/attention`-backed Poincaré op tests.
 *
 * Tests run with the optional peer dep MOCKED ABSENT so we exercise
 * the fallback path deterministically. The "real backend reachable"
 * path is exercised in the CI smoke against the actual native
 * binding (when CI's OS/arch has it).
 *
 * Acceptance:
 *   - All async ops return Float32Array of correct dimension
 *   - hyperbolicAttentionAvailable() returns boolean (no throw)
 *   - Fallback path produces values consistent with hand-rolled
 *     hyperbolic.ts (within numerical tolerance)
 *   - Default curvature = 1.0 when omitted
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';

// Force fallback path so tests don't depend on the native binding
// shipping correctly for the test runner's OS.
vi.mock('@ruvector/attention', () => ({}));

import {
  // The source module exports under the simple names; the package's
  // public index.ts re-exports with an `Async` suffix to distinguish
  // from the hand-rolled `hyperbolic.ts` sync versions. We import
  // direct here to test the source.
  projectToPoincareBall as projectToPoincareBallAsync,
  poincareDistance as poincareDistanceAsync,
  expMap as expMapAsync,
  logMap as logMapAsync,
  mobiusAddition as mobiusAdditionAsync,
  hyperbolicAttentionAvailable,
  __resetHyperbolicAttentionCache,
} from '../hyperbolic-attention.js';

beforeEach(() => {
  __resetHyperbolicAttentionCache();
});

describe('hyperbolic-attention — fallback path (no peer dep)', () => {
  it('hyperbolicAttentionAvailable returns false when peer dep absent', async () => {
    expect(await hyperbolicAttentionAvailable()).toBe(false);
  });

  it('projectToPoincareBallAsync returns a Float32Array of correct dim', async () => {
    const r = await projectToPoincareBallAsync(new Float32Array([0.3, 0.0, 0.0]));
    expect(r).toBeInstanceOf(Float32Array);
    expect(r.length).toBe(3);
  });

  it('projectToPoincareBallAsync accepts number[] input', async () => {
    const r = await projectToPoincareBallAsync([0.3, 0.0, 0.0]);
    expect(r).toBeInstanceOf(Float32Array);
    expect(r.length).toBe(3);
  });

  it('projectToPoincareBallAsync keeps small Euclidean vectors inside the ball', async () => {
    const r = await projectToPoincareBallAsync(new Float32Array([0.3, 0.0, 0.0]));
    let normSq = 0;
    for (let i = 0; i < r.length; i++) normSq += r[i]! * r[i]!;
    expect(Math.sqrt(normSq)).toBeLessThan(1);
  });

  it('poincareDistanceAsync returns a finite non-negative number', async () => {
    const d = await poincareDistanceAsync(
      new Float32Array([0.2, 0.0]),
      new Float32Array([0.0, 0.2]),
    );
    expect(typeof d).toBe('number');
    expect(d).toBeGreaterThanOrEqual(0);
    expect(Number.isFinite(d)).toBe(true);
  });

  it('poincareDistanceAsync gives 0 (or near-0) for identical points', async () => {
    const d = await poincareDistanceAsync(
      new Float32Array([0.1, 0.1, 0.0]),
      new Float32Array([0.1, 0.1, 0.0]),
    );
    expect(d).toBeLessThan(1e-6);
  });

  it('mobiusAdditionAsync returns a Float32Array', async () => {
    const r = await mobiusAdditionAsync(
      new Float32Array([0.1, 0.0]),
      new Float32Array([0.0, 0.1]),
    );
    expect(r).toBeInstanceOf(Float32Array);
    expect(r.length).toBe(2);
  });

  it('expMapAsync and logMapAsync return Float32Arrays of correct dim', async () => {
    const a = new Float32Array([0.2, 0.0, 0.0]);
    const tangent = new Float32Array([0.05, 0.05, 0.0]);
    const exp = await expMapAsync(a, tangent);
    expect(exp).toBeInstanceOf(Float32Array);
    expect(exp.length).toBe(3);

    const b = new Float32Array([0.3, 0.1, 0.0]);
    const log = await logMapAsync(a, b);
    expect(log).toBeInstanceOf(Float32Array);
    expect(log.length).toBe(3);
  });

  it('honors a custom curvature option', async () => {
    const d1 = await poincareDistanceAsync(
      new Float32Array([0.2, 0.0]),
      new Float32Array([0.0, 0.2]),
      { curvature: 1.0 },
    );
    const d2 = await poincareDistanceAsync(
      new Float32Array([0.2, 0.0]),
      new Float32Array([0.0, 0.2]),
      { curvature: 0.5 },
    );
    // Different curvatures produce different distances — exact values
    // depend on the backend, but they must differ.
    expect(d1).not.toBe(d2);
  });
});
