/**
 * Tests for vector/semantic search path coverage.
 *
 * The entire test suite previously set `semanticSearchMode: "off"`, so
 * `tryVecScores()` and the hybrid score merging pipeline were dead code
 * in tests. This file covers:
 *
 *  - tryVecScores runs when semantic status is ready
 *  - Hybrid score merging (FTS 0.7 + vec 0.3 weights)
 *  - FTS-only entries surviving in hybrid mode
 *  - NaN/Infinity guard on vector distances
 *  - BM25 normalization edge cases (all identical scores)
 *  - JS fallback path (BLOB-based cosine similarity, no sqlite-vec)
 *  - Dimension mismatch produces zero similarity
 */

import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
  closeDatabase,
  openDatabase,
  rebuildFts,
  searchFts,
  searchVec,
  setMeta,
  upsertEmbedding,
  upsertEntry,
} from "../src/indexer/db/db";
import type { StashEntry } from "../src/indexer/passes/metadata";
import { cosineSimilarity } from "../src/llm/embedder";
import type { Database } from "../src/storage/database";
import { type Cleanup, sandboxXdgCacheHome, sandboxXdgConfigHome } from "./_helpers/sandbox";

// ── Temp directory management ───────────────────────────────────────────────

const createdTmpDirs: string[] = [];

function createTmpDir(prefix = "akm-vec-"): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
  createdTmpDirs.push(dir);
  return dir;
}

afterAll(() => {
  for (const dir of createdTmpDirs) {
    fs.rmSync(dir, { recursive: true, force: true });
  }
});

// ── Helpers ─────────────────────────────────────────────────────────────────

function tmpDbPath(label = "vec"): string {
  const dir = createTmpDir(`akm-${label}-`);
  return path.join(dir, "test.db");
}

function makeEntry(overrides: Partial<StashEntry> & { name: string; type: string }): StashEntry {
  return {
    description: "A test entry",
    ...overrides,
  };
}

function insertTestEntry(
  db: Database,
  key: string,
  opts?: {
    dirPath?: string;
    filePath?: string;
    stashDir?: string;
    description?: string;
    searchText?: string;
    type?: string;
    tags?: string[];
  },
): number {
  const type = opts?.type ?? "script";
  const entry = makeEntry({
    name: key,
    type,
    description: opts?.description ?? `Description for ${key}`,
    tags: opts?.tags,
  });
  return upsertEntry(
    db,
    key,
    opts?.dirPath ?? "/test/dir",
    opts?.filePath ?? `/test/dir/${key}.ts`,
    opts?.stashDir ?? "/test/stash",
    entry,
    opts?.searchText ?? `${key} ${entry.description}`,
  );
}

/**
 * Create a normalized Float32 vector of the given dimension.
 * The vector has value `val` at each position, then is L2-normalized.
 */
function makeNormalizedVec(dim: number, val = 1): number[] {
  const raw = new Array(dim).fill(val);
  const norm = Math.sqrt(raw.reduce((s, v) => s + v * v, 0));
  return raw.map((v) => v / norm);
}

// ── Environment isolation ───────────────────────────────────────────────────

let envCleanup: Cleanup = () => {};

beforeEach(() => {
  const cacheResult = sandboxXdgCacheHome();
  const cfgResult = sandboxXdgConfigHome(cacheResult.cleanup);
  envCleanup = cfgResult.cleanup;
});

afterEach(() => {
  envCleanup();
  envCleanup = () => {};
});

// ── Test a: tryVecScores runs when status is ready ─────────────────────────

describe("tryVecScores activation", () => {
  test("searchVec returns results when embeddings exist in BLOB table", () => {
    // Verify the low-level searchVec (which delegates to searchBlobVec
    // when sqlite-vec is unavailable) returns results from the embeddings
    // BLOB table. This is the data path that tryVecScores consumes.
    const dbPath = tmpDbPath("vec-activation");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      const id = insertTestEntry(db, "vec-ready-tool", {
        description: "A tool with embeddings ready for vector search",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // Insert a normalized embedding into the BLOB table
      const embedding = makeNormalizedVec(dim);
      upsertEmbedding(db, id, embedding);
      setMeta(db, "hasEmbeddings", "1");

      // Query with the same vector — should find the entry
      const results = searchVec(db, embedding, 5);
      expect(results.length).toBe(1);
      expect(results[0].id).toBe(id);
      // Distance should be ~0 since query = stored embedding
      expect(results[0].distance).toBeLessThan(0.01);
    } finally {
      closeDatabase(db);
    }
  });

  test("searchVec returns results sorted by similarity (closest first)", () => {
    const dbPath = tmpDbPath("vec-sorted");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      // Insert two entries with different embeddings
      const id1 = insertTestEntry(db, "close-match", {
        description: "Close match entry",
        stashDir: "/test/stash",
      });
      const id2 = insertTestEntry(db, "far-match", {
        description: "Far match entry",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // close-match: embedding near the query direction
      const closeEmb = makeNormalizedVec(dim, 1); // [0.5, 0.5, 0.5, 0.5] normalized
      upsertEmbedding(db, id1, closeEmb);

      // far-match: embedding in a very different direction
      const farRaw = [1, 0, 0, 0]; // already unit
      upsertEmbedding(db, id2, farRaw);

      setMeta(db, "hasEmbeddings", "1");

      // Query with the same direction as close-match
      const queryVec = makeNormalizedVec(dim, 1);
      const results = searchVec(db, queryVec, 10);

      expect(results.length).toBe(2);
      // The close match should come first (smaller distance)
      const closeResult = results.find((r) => r.id === id1);
      const farResult = results.find((r) => r.id === id2);
      expect(closeResult).toBeDefined();
      expect(farResult).toBeDefined();
      expect(closeResult?.distance).toBeLessThan(farResult?.distance ?? Number.POSITIVE_INFINITY);
    } finally {
      closeDatabase(db);
    }
  });
});

// ── Test b: Hybrid score merging ───────────────────────────────────────────

describe("Hybrid score merging (FTS 0.7 + vec 0.3 weights)", () => {
  test("combined score uses FTS and vec weights correctly", () => {
    // Directly verify the weighted combination formula:
    //   combinedScore = ftsNormalized * 0.7 + vecCosine * 0.3
    // This mirrors the logic in searchDatabase() lines 228-237.
    const FTS_WEIGHT = 0.7;
    const VEC_WEIGHT = 0.3;

    const ftsNormalized = 0.8; // Hypothetical normalized FTS score
    const vecCosine = 0.9; // Hypothetical cosine similarity

    const expected = ftsNormalized * FTS_WEIGHT + vecCosine * VEC_WEIGHT;
    // 0.8 * 0.7 + 0.9 * 0.3 = 0.56 + 0.27 = 0.83
    expect(expected).toBeCloseTo(0.83, 2);

    // Verify the vec component matters: a high vec score should pull up
    // an entry with a lower FTS score
    const lowFts = 0.3;
    const highVec = 1.0;
    const boostedScore = lowFts * FTS_WEIGHT + highVec * VEC_WEIGHT;
    // 0.3 * 0.7 + 1.0 * 0.3 = 0.21 + 0.3 = 0.51
    expect(boostedScore).toBeCloseTo(0.51, 2);

    // And vice versa: high FTS with low vec
    const highFts = 1.0;
    const lowVec = 0.1;
    const ftsHeavy = highFts * FTS_WEIGHT + lowVec * VEC_WEIGHT;
    // 1.0 * 0.7 + 0.1 * 0.3 = 0.7 + 0.03 = 0.73
    expect(ftsHeavy).toBeCloseTo(0.73, 2);
  });

  test("FTS and vec rankings differ but combined score reflects both", () => {
    // Simulate the scenario where FTS and vec disagree on ranking.
    // Entry A: high FTS, low vec. Entry B: low FTS, high vec.
    // The combined scores should place them closer together than either
    // signal alone would suggest.
    const FTS_WEIGHT = 0.7;
    const VEC_WEIGHT = 0.3;

    // Entry A: FTS champion
    const aFts = 1.0;
    const aVec = 0.2;
    const aCombined = aFts * FTS_WEIGHT + aVec * VEC_WEIGHT;

    // Entry B: Vector champion
    const bFts = 0.4;
    const bVec = 1.0;
    const bCombined = bFts * FTS_WEIGHT + bVec * VEC_WEIGHT;

    // A should still win (FTS weight is higher), but B is close
    expect(aCombined).toBeGreaterThan(bCombined);
    // The gap should be smaller than FTS-only
    const ftsOnlyGap = aFts - bFts; // 0.6
    const combinedGap = aCombined - bCombined; // (0.76 - 0.58) = 0.18
    expect(combinedGap).toBeLessThan(ftsOnlyGap);
  });
});

// ── Test c: FTS-only entries in hybrid mode ────────────────────────────────

describe("FTS-only entries survive in hybrid mode", () => {
  test("entry matching FTS but with no embedding appears in results", () => {
    const dbPath = tmpDbPath("fts-only-hybrid");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      // Insert two entries: one with embedding, one without
      const idWithEmb = insertTestEntry(db, "with-embedding", {
        description: "A tool with vector support for deploy",
        searchText: "with-embedding deploy tool vector support",
        stashDir: "/test/stash",
      });
      const idNoEmb = insertTestEntry(db, "no-embedding", {
        description: "A deploy tool without vector support",
        searchText: "no-embedding deploy tool without vector",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // Only add embedding for the first entry
      const embedding = makeNormalizedVec(dim);
      upsertEmbedding(db, idWithEmb, embedding);
      setMeta(db, "hasEmbeddings", "1");

      // Both should appear in FTS results for "deploy"
      const ftsResults = searchFts(db, "deploy", 10);
      const ftsIds = ftsResults.map((r) => r.id);
      expect(ftsIds).toContain(idWithEmb);
      expect(ftsIds).toContain(idNoEmb);

      // The entry without an embedding still has a valid FTS score.
      // In the hybrid merging code, it gets rankingMode "fts" (not "hybrid").
      // Verify both entries found by FTS are valid.
      for (const result of ftsResults) {
        expect(Number.isFinite(result.bm25Score)).toBe(true);
        expect(Number.isNaN(result.bm25Score)).toBe(false);
      }
    } finally {
      closeDatabase(db);
    }
  });

  test("FTS results with no embedding counterpart get ftsScore only (no vec component)", () => {
    // Simulate the merging logic: when embedScoreMap has no entry for an id,
    // the combined score equals ftsScore alone (no vec weight added).
    const ftsScoreMap = new Map<number, number>();
    ftsScoreMap.set(1, 0.8); // has embedding
    ftsScoreMap.set(2, 0.6); // no embedding

    const embedScoreMap = new Map<number, number>();
    embedScoreMap.set(1, 0.9); // only entry 1 has vec score

    const FTS_WEIGHT = 0.7;
    const VEC_WEIGHT = 0.3;

    // Entry 1: hybrid score
    const entry1Embed = embedScoreMap.get(1);
    const entry1Fts = ftsScoreMap.get(1);
    if (entry1Fts === undefined) {
      throw new Error("Expected FTS score for entry 1");
    }
    const entry1Score = entry1Embed !== undefined ? entry1Fts * FTS_WEIGHT + entry1Embed * VEC_WEIGHT : entry1Fts;
    expect(entry1Score).toBeCloseTo(0.8 * 0.7 + 0.9 * 0.3, 4);

    // Entry 2: FTS-only score (no vec component)
    const entry2Embed = embedScoreMap.get(2);
    const entry2Fts = ftsScoreMap.get(2);
    if (entry2Fts === undefined) {
      throw new Error("Expected FTS score for entry 2");
    }
    const entry2Score = entry2Embed !== undefined ? entry2Fts * FTS_WEIGHT + entry2Embed * VEC_WEIGHT : entry2Fts;
    expect(entry2Score).toBe(0.6); // Pure FTS score, no weighting
  });
});

// ── Test d: NaN/Infinity guard ─────────────────────────────────────────────

describe("NaN/Infinity guard on vector distances", () => {
  test("NaN distance is clamped to 0 by the guard formula", () => {
    // The guard in tryVecScores:
    //   const raw = 1 - (distance * distance) / 2;
    //   scores.set(id, Number.isFinite(raw) ? Math.max(0, raw) : 0);
    const distance = Number.NaN;
    const raw = 1 - (distance * distance) / 2;
    expect(Number.isNaN(raw)).toBe(true);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBe(0);
  });

  test("Infinity distance is clamped to 0 by the guard formula", () => {
    const distance = Number.POSITIVE_INFINITY;
    const raw = 1 - (distance * distance) / 2;
    expect(Number.isFinite(raw)).toBe(false);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBe(0);
  });

  test("negative Infinity distance is clamped to 0", () => {
    const distance = Number.NEGATIVE_INFINITY;
    const raw = 1 - (distance * distance) / 2;
    expect(Number.isFinite(raw)).toBe(false);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBe(0);
  });

  test("large distance producing negative raw is clamped to 0", () => {
    // distance = 3 => raw = 1 - 9/2 = 1 - 4.5 = -3.5
    const distance = 3;
    const raw = 1 - (distance * distance) / 2;
    expect(raw).toBeLessThan(0);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBe(0);
  });

  test("normal distance 0 produces cosine similarity of 1", () => {
    const distance = 0;
    const raw = 1 - (distance * distance) / 2;
    expect(raw).toBe(1);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBe(1);
  });

  test("normal distance ~1.414 (orthogonal vectors) produces cosine ~0", () => {
    // For orthogonal unit vectors, L2 distance = sqrt(2) ~ 1.414
    const distance = Math.sqrt(2);
    const raw = 1 - (distance * distance) / 2;
    // raw = 1 - 2/2 = 0
    expect(raw).toBeCloseTo(0, 5);
    const guarded = Number.isFinite(raw) ? Math.max(0, raw) : 0;
    expect(guarded).toBeCloseTo(0, 5);
  });
});

// ── Test e: BM25 normalization edge cases ──────────────────────────────────

describe("BM25 normalization edge cases", () => {
  test("all identical BM25 scores normalize to 1.0", () => {
    // Mirrors the normalization logic in searchDatabase():
    //   const range = bestBm25 - worstBm25;
    //   const normalized = range !== 0 ? (r.bm25Score - worstBm25) / range : 1.0;
    //   const ftsScore = 0.3 + normalized * 0.7;
    const scores = [-5.0, -5.0, -5.0]; // all identical
    const bestBm25 = scores[0];
    const worstBm25 = scores[scores.length - 1];
    const range = bestBm25 - worstBm25; // 0

    const normalized = scores.map((s) => (range !== 0 ? (s - worstBm25) / range : 1.0));

    // All should be 1.0 when range is 0
    for (const n of normalized) {
      expect(n).toBe(1.0);
    }

    // After scaling to 0.3-1.0 range
    const scaled = normalized.map((n) => 0.3 + n * 0.7);
    for (const s of scaled) {
      expect(s).toBe(1.0);
    }
  });

  test("two distinct BM25 scores normalize to 1.0 and 0.3", () => {
    // Best = -10 (most negative), worst = -2 (least negative)
    const bestBm25 = -10;
    const worstBm25 = -2;
    const range = bestBm25 - worstBm25; // -8

    const bestNormalized = (bestBm25 - worstBm25) / range;
    expect(bestNormalized).toBe(1.0);

    const worstNormalized = (worstBm25 - worstBm25) / range;
    expect(worstNormalized).toBeCloseTo(0, 10);

    // After scaling
    const bestScaled = 0.3 + bestNormalized * 0.7;
    expect(bestScaled).toBe(1.0);

    const worstScaled = 0.3 + worstNormalized * 0.7;
    expect(worstScaled).toBe(0.3);
  });

  test("BM25 normalization with three scores preserves ordering", () => {
    // best = -15, mid = -10, worst = -5
    const bestBm25 = -15;
    const worstBm25 = -5;
    const range = bestBm25 - worstBm25; // -10

    const bestN = (bestBm25 - worstBm25) / range; // (-15 - -5) / -10 = -10/-10 = 1.0
    const midN = (-10 - worstBm25) / range; // (-10 - -5) / -10 = -5/-10 = 0.5
    const worstN = (worstBm25 - worstBm25) / range; // 0 / -10 = -0

    expect(bestN).toBe(1.0);
    expect(midN).toBe(0.5);
    expect(worstN).toBeCloseTo(0, 10);

    // Ordering is preserved after scaling
    const bestS = 0.3 + bestN * 0.7;
    const midS = 0.3 + midN * 0.7;
    const worstS = 0.3 + worstN * 0.7;

    expect(bestS).toBeGreaterThan(midS);
    expect(midS).toBeGreaterThan(worstS);
  });

  test("single FTS result normalizes to 1.0", () => {
    // When there's only one result, best = worst, range = 0
    const scores = [-7.3];
    const bestBm25 = scores[0];
    const worstBm25 = scores[0];
    const range = bestBm25 - worstBm25; // 0

    const normalized = range !== 0 ? (scores[0] - worstBm25) / range : 1.0;
    expect(normalized).toBe(1.0);

    const scaled = 0.3 + normalized * 0.7;
    expect(scaled).toBe(1.0);
  });
});

// ── Test f: JS fallback path (BLOB-based cosine similarity) ────────────────

describe("JS fallback path (BLOB cosine similarity, no sqlite-vec)", () => {
  test("searchVec with BLOB embeddings returns correct similarity ranking", () => {
    const dbPath = tmpDbPath("blob-fallback");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      // Insert three entries with different embeddings
      const id1 = insertTestEntry(db, "exact-match", {
        description: "Exact match entry",
        stashDir: "/test/stash",
      });
      const id2 = insertTestEntry(db, "partial-match", {
        description: "Partial match entry",
        stashDir: "/test/stash",
      });
      const id3 = insertTestEntry(db, "no-match", {
        description: "No match entry",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // Embeddings with known cosine similarities to query [1, 0, 0, 0]:
      // exact-match: [1, 0, 0, 0] -> cosine = 1.0
      // partial-match: [0.707, 0.707, 0, 0] -> cosine ~ 0.707
      // no-match: [0, 0, 0, 1] -> cosine = 0.0
      upsertEmbedding(db, id1, [1, 0, 0, 0]);
      const partial = Math.SQRT1_2;
      upsertEmbedding(db, id2, [partial, partial, 0, 0]);
      upsertEmbedding(db, id3, [0, 0, 0, 1]);
      setMeta(db, "hasEmbeddings", "1");

      const queryVec = [1, 0, 0, 0];
      const results = searchVec(db, queryVec, 10);

      expect(results.length).toBe(3);

      // Results should be sorted by similarity descending (distance ascending)
      // The JS fallback converts cosine similarity to L2 distance:
      // For normalized vectors: L2 = sqrt(2 * (1 - cos_sim))
      const exactResult = results.find((r) => r.id === id1);
      const partialResult = results.find((r) => r.id === id2);
      const noMatchResult = results.find((r) => r.id === id3);

      expect(exactResult).toBeDefined();
      expect(partialResult).toBeDefined();
      expect(noMatchResult).toBeDefined();

      // exact match should have smallest distance
      expect(exactResult?.distance).toBeLessThan(partialResult?.distance ?? Number.POSITIVE_INFINITY);
      expect(partialResult?.distance).toBeLessThan(noMatchResult?.distance ?? Number.POSITIVE_INFINITY);

      // exact match distance should be ~0
      expect(exactResult?.distance).toBeCloseTo(0, 2);
      // no-match distance should be ~sqrt(2) ~ 1.414
      expect(noMatchResult?.distance).toBeCloseTo(Math.sqrt(2), 1);
    } finally {
      closeDatabase(db);
    }
  });

  test("searchVec returns empty array when no embeddings exist", () => {
    const dbPath = tmpDbPath("blob-empty");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      insertTestEntry(db, "no-embed-entry", {
        description: "Entry without embedding",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      const results = searchVec(db, [1, 0, 0, 0], 10);
      expect(results).toHaveLength(0);
    } finally {
      closeDatabase(db);
    }
  });

  test("searchVec with k smaller than total results returns top-k", () => {
    const dbPath = tmpDbPath("blob-topk");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      // Insert 5 entries with embeddings
      const ids: number[] = [];
      for (let i = 0; i < 5; i++) {
        const id = insertTestEntry(db, `entry-${i}`, {
          description: `Entry number ${i}`,
          stashDir: "/test/stash",
        });
        // Each entry has a slightly different embedding direction
        const emb = [0, 0, 0, 0];
        emb[i % dim] = 1;
        upsertEmbedding(db, id, emb);
        ids.push(id);
      }
      rebuildFts(db);
      setMeta(db, "hasEmbeddings", "1");

      // Query for top 2 only
      const results = searchVec(db, [1, 0, 0, 0], 2);
      expect(results.length).toBe(2);
    } finally {
      closeDatabase(db);
    }
  });
});

// ── Test g: Dimension mismatch produces zero ───────────────────────────────

describe("Dimension mismatch produces zero similarity", () => {
  test("cosineSimilarity returns 0 for mismatched dimensions (384 vs 768)", () => {
    const vec384 = new Array(384).fill(1 / Math.sqrt(384));
    const vec768 = new Array(768).fill(1 / Math.sqrt(768));

    const similarity = cosineSimilarity(vec384, vec768);
    expect(similarity).toBe(0);
  });

  test("cosineSimilarity returns 0 for mismatched dimensions (small vectors)", () => {
    const vecA = [1, 0, 0];
    const vecB = [1, 0, 0, 0];

    const similarity = cosineSimilarity(vecA, vecB);
    expect(similarity).toBe(0);
  });

  test("cosineSimilarity returns correct value for matching dimensions", () => {
    // Same direction: cosine = 1.0
    const vecA = [1, 0, 0, 0];
    const vecB = [1, 0, 0, 0];
    expect(cosineSimilarity(vecA, vecB)).toBeCloseTo(1.0, 5);

    // Orthogonal: cosine = 0.0
    const vecC = [1, 0, 0, 0];
    const vecD = [0, 1, 0, 0];
    expect(cosineSimilarity(vecC, vecD)).toBeCloseTo(0.0, 5);

    // Opposite: cosine = -1.0
    const vecE = [1, 0, 0, 0];
    const vecF = [-1, 0, 0, 0];
    expect(cosineSimilarity(vecE, vecF)).toBeCloseTo(-1.0, 5);
  });

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

  test("cosineSimilarity returns 0 for zero vectors", () => {
    const zero = [0, 0, 0, 0];
    expect(cosineSimilarity(zero, zero)).toBe(0);
  });

  test("searchBlobVec handles dimension mismatch gracefully via cosineSimilarity", () => {
    // When stored embeddings have 4 dims but query has 8 dims,
    // the JS fallback calls cosineSimilarity which returns 0.
    // Verify this end-to-end via searchVec.
    const dbPath = tmpDbPath("dim-mismatch");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      const id = insertTestEntry(db, "small-emb", {
        description: "Entry with small embedding",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // Store a 4-dim embedding
      upsertEmbedding(db, id, [1, 0, 0, 0]);
      setMeta(db, "hasEmbeddings", "1");

      // Query with an 8-dim vector (dimension mismatch)
      const queryVec8 = [1, 0, 0, 0, 0, 0, 0, 0];
      const results = searchVec(db, queryVec8, 10);

      // Results should still come back (no crash) but with max distance
      // since cosineSimilarity returns 0 for mismatched dims.
      // The JS fallback converts cosine=0 to L2 = sqrt(2*(1-0)) = sqrt(2).
      if (results.length > 0) {
        expect(results[0].distance).toBeCloseTo(Math.sqrt(2), 1);
      }
      // Either we get the result with max distance or empty (both acceptable)
      expect(results.length).toBeLessThanOrEqual(1);
    } finally {
      closeDatabase(db);
    }
  });
});

// ── End-to-end: L2-to-cosine conversion round-trip ─────────────────────────

describe("L2-to-cosine conversion round-trip", () => {
  test("searchVec distance converts correctly back to cosine similarity", () => {
    // The scoring pipeline in tryVecScores does:
    //   raw = 1 - (distance * distance) / 2
    // And searchBlobVec does:
    //   distance = sqrt(2 * max(0, 1 - cosineSim))
    // These should be inverse operations for normalized vectors.
    const dbPath = tmpDbPath("roundtrip");
    const dim = 4;
    const db = openDatabase(dbPath, { embeddingDim: dim });
    try {
      const id = insertTestEntry(db, "roundtrip-entry", {
        description: "Round-trip test entry",
        stashDir: "/test/stash",
      });
      rebuildFts(db);

      // Known cosine similarity: query=[1,0,0,0], stored=[0.6,0.8,0,0]
      // cos(query, stored) = 0.6
      upsertEmbedding(db, id, [0.6, 0.8, 0, 0]);
      setMeta(db, "hasEmbeddings", "1");

      const results = searchVec(db, [1, 0, 0, 0], 10);
      expect(results.length).toBe(1);

      const distance = results[0].distance;
      // Convert back: cosine = 1 - distance^2 / 2
      const recoveredCosine = 1 - (distance * distance) / 2;
      expect(recoveredCosine).toBeCloseTo(0.6, 1);
    } finally {
      closeDatabase(db);
    }
  });
});
