#!/usr/bin/env node
/**
 * local-embeddings.mjs — real-runtime verifier for LocalEmbeddingProvider.
 *
 * WHY THIS LIVES OUTSIDE JEST
 *
 *   Transformers.js delegates inference to ONNX Runtime, which validates
 *   typed-array inputs with `instanceof Float32Array`. Jest's
 *   `--experimental-vm-modules` loader runs each test file in its own
 *   VM context where `Float32Array` is a different constructor reference
 *   than ORT's check sees. Every real inference call throws:
 *
 *     TypeError: A float32 tensor's data must be type of function Float32Array()
 *
 *   That's a fundamental incompatibility between Jest's experimental
 *   ESM execution model and native bindings. The unit tests in
 *   `localProvider.test.js` paper over this with a stub `pipelineFactory`,
 *   which is why they "pass" while exercising none of the real path.
 *
 *   So we verify the real path here, in a plain `node` process — same
 *   realm everywhere, ORT happy, end-to-end behavior actually checked.
 *
 * WHAT IT VERIFIES
 *
 *   1. The provider warms up against a real ONNX model.
 *   2. `embedQuery` returns a normalized Float32Array of the right dim.
 *   3. Same input → same vector (determinism).
 *   4. Cosine similarity makes semantic sense (paraphrase > unrelated).
 *   5. BGE task prefix actually changes the vector (query vs document).
 *   6. `EmbeddingService → LocalEmbeddingProvider` wiring works.
 *
 * RUN
 *
 *   npm run verify:local-embeddings
 *
 *   First run downloads ~30 MB of weights for bge-small-en-v1.5. Cached
 *   thereafter (~5s subsequent). Exits 0 on success, 1 on failure.
 */

import os from 'os';
import fsp from 'fs/promises';
import path from 'path';
import { LocalEmbeddingProvider } from '../../src/services/embeddings/providers/localProvider.js';
import { EmbeddingService } from '../../src/services/embeddings/embeddingService.js';

const TEST_MODEL = 'Xenova/bge-small-en-v1.5';
const TEST_DIMS = 384;

const results = [];
let failures = 0;

function check(name, fn) {
  return (async () => {
    const t0 = Date.now();
    try {
      await fn();
      const ms = Date.now() - t0;
      results.push({ name, ok: true, ms });
      console.log(`  ✔ ${name} (${ms}ms)`);
    } catch (err) {
      failures++;
      const ms = Date.now() - t0;
      results.push({ name, ok: false, ms, err: err?.message });
      console.error(`  ✗ ${name} (${ms}ms)`);
      console.error(`      ${err?.stack || err}`);
    }
  })();
}

function dot(a, b) {
  let s = 0;
  for (let i = 0; i < a.length; i++) s += a[i] * b[i];
  return s;
}

async function main() {
  console.log('Local-embeddings real-runtime verifier');
  console.log(`Model: ${TEST_MODEL} (${TEST_DIMS}d)`);
  console.log('');

  // Verify the package is importable first. If not, every subsequent
  // assertion is meaningless, so fail loud and bail.
  try {
    await import('@xenova/transformers');
  } catch (err) {
    console.error('FAIL: @xenova/transformers is not importable.');
    console.error(`      ${err?.message || err}`);
    console.error('      It must live in devDependencies; run `npm install`.');
    process.exit(1);
  }

  const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'loxia-local-verify-'));
  const cacheDir = path.join(tmpDir, 'cache');
  const sharedProvider = new LocalEmbeddingProvider({ modelId: TEST_MODEL, cacheDir });

  await check('warmup + health', async () => {
    await sharedProvider.warmup();
    const h = await sharedProvider.health();
    if (!h.ok) throw new Error(`health.ok=${h.ok}`);
  });

  await check('embedQuery returns normalized Float32Array of correct dim', async () => {
    const vec = await sharedProvider.embedQuery('hello world');
    if (!(vec instanceof Float32Array)) throw new Error('not a Float32Array');
    if (vec.length !== TEST_DIMS) throw new Error(`dim=${vec.length}, expected ${TEST_DIMS}`);
    let sum = 0;
    for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
    const mag = Math.sqrt(sum);
    if (Math.abs(mag - 1) > 1e-4) throw new Error(`magnitude=${mag.toFixed(6)}, expected ≈1`);
  });

  await check('deterministic: same input → same vector', async () => {
    const v1 = await sharedProvider.embedQuery('reproducibility check');
    const v2 = await sharedProvider.embedQuery('reproducibility check');
    let maxDelta = 0;
    for (let i = 0; i < v1.length; i++) {
      const d = Math.abs(v1[i] - v2[i]);
      if (d > maxDelta) maxDelta = d;
    }
    if (maxDelta >= 1e-5) throw new Error(`maxDelta=${maxDelta} (expected <1e-5)`);
  });

  await check('cosine similarity is semantically meaningful', async () => {
    const seed       = await sharedProvider.embedQuery('how do I throttle rate-limited API requests?');
    const paraphrase = await sharedProvider.embedQuery('handling rate limits in HTTP API clients');
    const unrelated  = await sharedProvider.embedQuery('recipe for chocolate chip cookies');
    const sp = dot(seed, paraphrase);
    const su = dot(seed, unrelated);
    if (sp <= su) throw new Error(`paraphrase(${sp.toFixed(3)}) ≤ unrelated(${su.toFixed(3)})`);
    if ((sp - su) <= 0.05) throw new Error(`gap=${(sp - su).toFixed(3)} too small (expected >0.05)`);
  });

  await check('BGE task prefix differentiates query from document', async () => {
    const text = 'How do I configure retries?';
    const [asQuery]    = await sharedProvider.embedDocuments([text], 'query');
    const [asDocument] = await sharedProvider.embedDocuments([text], 'document');
    let maxDelta = 0;
    for (let i = 0; i < asQuery.length; i++) {
      const d = Math.abs(asQuery[i] - asDocument[i]);
      if (d > maxDelta) maxDelta = d;
    }
    if (maxDelta <= 1e-3) throw new Error(`maxDelta=${maxDelta} (expected >1e-3)`);
  });

  await check('EmbeddingService → LocalEmbeddingProvider wiring', async () => {
    const svc = new EmbeddingService({
      config: { provider: 'local', local: { modelId: TEST_MODEL } },
      apiKeyManager: { getEmbeddingApiKey: () => null },
      baseUrl: 'https://unused-for-local',
      stateDir: tmpDir,
    });
    if (!svc.isEnabled) throw new Error('svc.isEnabled=false');
    const vec = await svc.embedQuery('end-to-end through embeddingService');
    if (!(vec instanceof Float32Array)) throw new Error('svc returned non-Float32Array');
    if (vec.length !== TEST_DIMS) throw new Error(`svc.dim=${vec.length}`);
    const t = svc.getTelemetry();
    if (!(t.callsTotal > 0)) throw new Error(`telemetry.callsTotal=${t.callsTotal}`);
    if (!(t.tokensTotal > 0)) throw new Error(`telemetry.tokensTotal=${t.tokensTotal}`);
  });

  console.log('');
  const passed = results.filter(r => r.ok).length;
  const total = results.length;
  console.log(`${failures === 0 ? 'PASS' : 'FAIL'}: ${passed}/${total} checks passed`);
  if (failures > 0) process.exit(1);
}

main().catch(err => {
  console.error('VERIFIER CRASHED:', err);
  process.exit(1);
});
