#!/usr/bin/env node
/**
 * azure-custom-embeddings.mjs — real-network verifier for the
 * Azure-AI-Foundry-OpenAI-compat embedding deployment (e.g. Cohere
 * embed-v-4-0).
 *
 * WHY THIS LIVES OUTSIDE JEST
 *
 *   The unit tests in `azureCustomProvider.test.js` mock global `fetch`
 *   and verify the request shape (URL normalization, Authorization
 *   header, encoding_format=float, batch chunking). They cannot tell
 *   you that the deployment ACTUALLY responds with what the provider
 *   expects, or that the response parser handles the live shape (which
 *   varies subtly between OpenAI's hosted API and Azure's compat layer
 *   between model versions).
 *
 *   So this verifier runs against the real endpoint, just like a smoke
 *   test in production. It is the only thing that catches:
 *     - Deployment URL drift
 *     - API key rotation/expiry
 *     - Response schema drift (Cohere vs OpenAI-style)
 *     - Dimension mismatches between settings and actual deployment
 *
 * REQUIRED ENV
 *
 *   LOXIA_AZURE_CUSTOM_ENDPOINT     e.g. https://loxia-ai-resource.services.ai.azure.com/openai/v1/
 *   LOXIA_AZURE_CUSTOM_DEPLOYMENT   e.g. embed-v-4-0
 *   LOXIA_AZURE_CUSTOM_API_KEY      the deployment's API key
 *
 * OPTIONAL ENV
 *
 *   LOXIA_AZURE_CUSTOM_DIMS         declared dimensions (default 1536)
 *
 * RUN
 *
 *   npm run verify:azure-custom-embeddings
 *
 *   Exit codes:
 *     0  all checks passed
 *     1  one or more checks failed
 *     2  required env not set (so CI/dev can detect "not configured"
 *        explicitly rather than treating as pass)
 *
 *   The runner intentionally exits 2 (not 0) when unconfigured. A
 *   silent pass-on-missing is the same "low coverage = no test"
 *   anti-pattern we just removed for local embeddings: CI must
 *   either run this with secrets, or explicitly route around it.
 */

import { AzureCustomEmbeddingProvider } from '../../src/services/embeddings/providers/azureCustomProvider.js';

const endpoint   = process.env.LOXIA_AZURE_CUSTOM_ENDPOINT;
const deployment = process.env.LOXIA_AZURE_CUSTOM_DEPLOYMENT;
const apiKey     = process.env.LOXIA_AZURE_CUSTOM_API_KEY;
const dims       = Number(process.env.LOXIA_AZURE_CUSTOM_DIMS || 1536);

console.log('Azure-custom embeddings real-network verifier');
if (!endpoint || !deployment || !apiKey) {
  console.error('');
  console.error('SKIP: required env vars not set.');
  console.error('  LOXIA_AZURE_CUSTOM_ENDPOINT:    ' + (endpoint   ? 'set' : 'MISSING'));
  console.error('  LOXIA_AZURE_CUSTOM_DEPLOYMENT:  ' + (deployment ? 'set' : 'MISSING'));
  console.error('  LOXIA_AZURE_CUSTOM_API_KEY:     ' + (apiKey     ? 'set' : 'MISSING'));
  console.error('');
  console.error('CI must either configure these secrets and re-run, or');
  console.error('explicitly skip this step (and accept the coverage gap).');
  process.exit(2);
}

console.log(`Endpoint:   ${endpoint}`);
console.log(`Deployment: ${deployment}`);
console.log(`Dimensions: ${dims}`);
console.log('');

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;
}

const provider = new AzureCustomEmbeddingProvider({
  endpoint,
  deployment,
  getApiKey: () => apiKey,
  dimensions: dims,
});

await check('single embedQuery returns Float32Array of declared dim', async () => {
  const vec = await provider.embedQuery('hello world');
  if (!(vec instanceof Float32Array)) throw new Error('not a Float32Array');
  if (vec.length !== dims) {
    throw new Error(`server returned ${vec.length}d but settings declared ${dims}d`);
  }
  // Normalized by base class — magnitude ≈ 1.
  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-3) throw new Error(`magnitude=${mag.toFixed(6)} (expected ≈1)`);
});

await check('batch embedDocuments returns one vector per input', async () => {
  const inputs = ['retry logic', 'rate limiting', 'circuit breakers'];
  const vecs = await provider.embedDocuments(inputs);
  if (!Array.isArray(vecs)) throw new Error('not an array');
  if (vecs.length !== inputs.length) throw new Error(`got ${vecs.length} vectors for ${inputs.length} inputs`);
  for (let i = 0; i < vecs.length; i++) {
    if (!(vecs[i] instanceof Float32Array)) throw new Error(`vec[${i}] not Float32Array`);
    if (vecs[i].length !== dims) throw new Error(`vec[${i}].length=${vecs[i].length}, expected ${dims}`);
  }
});

await check('cosine similarity is semantically meaningful', async () => {
  const seed       = await provider.embedQuery('how do I throttle rate-limited API requests?');
  const paraphrase = await provider.embedQuery('handling rate limits in HTTP API clients');
  const unrelated  = await provider.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.03) throw new Error(`gap=${(sp - su).toFixed(3)} too small (expected >0.03)`);
});

await check('provider.health reports ok', async () => {
  const h = await provider.health();
  if (!h.ok) throw new Error(`health.ok=${h.ok} (${h.reason || 'no reason'})`);
});

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