import { describe, it, expect } from "vitest";
import { bm25Only } from "../index.js";

/**
 * `bm25Only` accepts an optional `labels` array. When set, the
 * Cypher gates results to nodes carrying at least one of the requested labels
 * (matches the hybrid vector half's per-index filter at index.ts:286-295).
 *
 * Pre-Task-747: `bm25Only` ignored labels entirely, so the admin route's
 * Ollama-down fallback returned BM25 hits across every label population — a
 * silent second-order failure on top of the loud-failure violation that the
 * mode banner addresses on the client.
 */

function makeStubSession(records: Array<Record<string, unknown>> = []) {
  const calls: Array<{ query: string; params: Record<string, unknown> }> = [];
  const session = {
    run(query: string, params: Record<string, unknown>) {
      calls.push({ query, params });
      return Promise.resolve({
        records: records.map((r) => ({ get: (k: string) => r[k] })),
      });
    },
  } as unknown as import("neo4j-driver").Session;
  return { session, calls };
}

describe("bm25Only — label gate", () => {
  it("omits the label clause when labels arg is absent (back-compat)", async () => {
    const { session, calls } = makeStubSession();
    await bm25Only(session, { query: "x", accountId: "acc-1", limit: 10 });
    expect(calls[0].query).not.toContain("$labels");
    expect(calls[0].params).not.toHaveProperty("labels");
  });

  it("omits the label clause when labels arg is an empty array", async () => {
    // Empty allowlist is treated as "no gate" rather than "match nothing" —
    // mirrors the hybrid vector half's `if (labels && labels.length > 0)`
    // guard so the two halves degrade identically.
    const { session, calls } = makeStubSession();
    await bm25Only(session, {
      query: "x",
      accountId: "acc-1",
      limit: 10,
      labels: [],
    });
    expect(calls[0].query).not.toContain("$labels");
    expect(calls[0].params).not.toHaveProperty("labels");
  });

  it("adds the label clause and param when labels arg is set", async () => {
    const { session, calls } = makeStubSession();
    await bm25Only(session, {
      query: "x",
      accountId: "acc-1",
      limit: 10,
      labels: ["Person"],
    });
    expect(calls[0].query).toContain("any(l IN labels(node) WHERE l IN $labels)");
    expect(calls[0].params.labels).toEqual(["Person"]);
  });

  it("passes the full labels array as a single param (multi-label)", async () => {
    const { session, calls } = makeStubSession();
    await bm25Only(session, {
      query: "x",
      accountId: "acc-1",
      limit: 10,
      labels: ["Person", "KnowledgeDocument"],
    });
    expect(calls[0].params.labels).toEqual(["Person", "KnowledgeDocument"]);
  });

  it("composes label gate alongside scope and agent clauses", async () => {
    const { session, calls } = makeStubSession();
    await bm25Only(session, {
      query: "x",
      accountId: "acc-1",
      limit: 10,
      labels: ["Person"],
      allowedScopes: ["public"],
      agentSlug: "support",
    });
    expect(calls[0].query).toContain("$allowedScopes");
    expect(calls[0].query).toContain("$agentSlug");
    expect(calls[0].query).toContain("$labels");
  });
});
