/**
 * PR-3.5: FTS5 Search Adapter Test
 * 
 * Run: node --test test/memory-core/fts5-adapter.test.mjs
 * 
 * Tests:
 *   1. FTS5 adapter is importable
 *   2. FTS5 adapter opens DB read-only
 *   3. FTS5 query returns unified contract
 *   4. FTS5 results have stable fields
 *   5. Empty query returns INVALID_QUERY
 *   6. Fallback to file-search when FTS5 unavailable
 *   7. Does not modify SQLite
 *   8. Does not modify MEMORY.md
 *   9. Production check still passes
 */

import { describe, it, before } from "node:test";
import assert from "node:assert";
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
const ROOT = resolve(__dirname);
const DB_PATH = join(homedir(), ".openclaw", "memory", "main.sqlite");

let createFTS5Adapter;
let adapter;
let fts5Available;

before(async () => {
  const mod = await import(
    join(ROOT, "src", "memory-core", "memory-fts5-search-adapter.mjs")
  );
  createFTS5Adapter = mod.createFTS5SearchAdapter;
  adapter = createFTS5Adapter(DB_PATH);
  fts5Available = await adapter.isAvailable();
});

describe("PR-3.5: FTS5 Search Adapter", () => {

  // ─── Test 1 ──────────────────────────────────────────
  it("adapter is importable", () => {
    assert.ok(typeof createFTS5Adapter === "function",
      "createFTS5SearchAdapter should be a function");
    assert.ok(typeof adapter.search === "function",
      "adapter.search should be a function");
    assert.ok(typeof adapter.isAvailable === "function",
      "adapter.isAvailable should be a function");
  });

  // ─── Test 2 ──────────────────────────────────────────
  it("adapter opens DB read-only", async () => {
    // FTS5 availability check
    const available = await adapter.isAvailable();
    assert.equal(typeof available, "boolean");
    
    if (available) {
      // Verify we can search (proves read-only connection works)
      const result = await adapter.search("test", { maxResults: 1 });
      // Either ok:true (found results) or ok:true (no results) 
      // or ok:false (FTS5 error on that term) — all acceptable
      assert.ok(typeof result.ok === "boolean", "should have ok field");
      assert.equal(result.source, "fts5-search",
        `source should be fts5-search, got ${result.source}`);
    } else {
      console.log("  FTS5 not available on this system — skipping read test");
    }
  });

  // ─── Test 3 ──────────────────────────────────────────
  it("FTS5 query returns unified contract", async () => {
    const result = await adapter.search("memory", { maxResults: 5 });
    
    assert.equal(result.ok, true, `search should succeed: ${JSON.stringify(result.error)}`);
    assert.ok(Array.isArray(result.results), "results should be array");
    assert.ok(result.results.length > 0, "should find results for 'memory'");
    assert.equal(result.source, "fts5-search");
    assert.ok(typeof result.latencyMs === "number", "latencyMs should be number");
    assert.ok(result.latencyMs >= 0, "latencyMs should be >= 0");
  });

  // ─── Test 4 ──────────────────────────────────────────
  it("FTS5 results have stable fields", async () => {
    const result = await adapter.search("OpenClaw", { maxResults: 5 });
    
    assert.equal(result.ok, true);
    assert.ok(result.results.length > 0, "should find OpenClaw references");
    
    for (const r of result.results) {
      assert.ok(typeof r.id === "string", `result.id should be string, got ${typeof r.id}`);
      assert.ok(typeof r.path === "string", `result.path should be string`);
      assert.ok(typeof r.snippet === "string", `result.snippet should be string`);
      assert.ok(typeof r.score === "number", `result.score should be number`);
      assert.ok(r.score >= 0 && r.score <= 1, `score should be 0-1, got ${r.score}`);
      assert.ok(typeof r.source === "string", `result.source should be string`);
    }
  });

  // ─── Test 5 ──────────────────────────────────────────
  it("empty query returns INVALID_QUERY", async () => {
    const result = await adapter.search("");
    
    assert.equal(result.ok, false, "empty query should fail");
    assert.equal(result.error.code, "INVALID_QUERY",
      `Expected INVALID_QUERY, got ${result.error?.code}`);
    assert.ok(Array.isArray(result.results), "results should be array");
    assert.equal(result.results.length, 0, "results should be empty");
    assert.equal(result.source, "fts5-search");
  });

  // ─── Test 6 ──────────────────────────────────────────
  it("fallback to file-search when FTS5 path is invalid", async () => {
    // Create adapter pointing to non-existent DB path
    const badAdapter = createFTS5Adapter("/nonexistent/path/main.sqlite");
    
    // Should return error without fallback
    const result = await badAdapter.search("test", { maxResults: 3 });
    
    assert.equal(result.ok, false, "should fail without DB");
    assert.equal(result.source, "file-search-fallback",
      "source should indicate fallback attempt");
    assert.ok(result.error, "should have error");
  });

  // ─── Test 7 ──────────────────────────────────────────
  it("does not modify SQLite file", async () => {
    const statBefore = existsSync(DB_PATH) ? readFileSync(DB_PATH, null).length : -1;
    
    // Run multiple searches
    await adapter.search("memory", { maxResults: 10 });
    await adapter.search("agent", { maxResults: 10 });
    await adapter.search("config", { maxResults: 10 });
    await adapter.search("");
    
    const statAfter = existsSync(DB_PATH) ? readFileSync(DB_PATH, null).length : -1;
    
    assert.equal(statAfter, statBefore,
      `SQLite file size changed: ${statBefore} → ${statAfter}`);
  });

  // ─── Test 8 ──────────────────────────────────────────
  it("does not modify MEMORY.md", async () => {
    const before = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
    
    // Run queries
    await adapter.search("MEMORY.md", { maxResults: 5 });
    await adapter.search("memory system", { maxResults: 5 });
    
    const after = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
    
    assert.equal(after, before, "MEMORY.md must not be modified");
  });

  // ─── Test 9 ──────────────────────────────────────────
  it("multiple queries are consistent", async () => {
    const r1 = await adapter.search("architecture", { maxResults: 5 });
    const r2 = await adapter.search("architecture", { maxResults: 5 });
    
    assert.equal(r1.ok, r2.ok);
    assert.equal(r1.source, r2.source);
    assert.equal(r1.results.length, r2.results.length,
      "same query should return same count");
    
    if (r1.results.length > 0 && r2.results.length > 0) {
      assert.equal(r1.results[0].id, r2.results[0].id,
        "same query should return same top result");
    }
  });

  // ─── Test 10 ─────────────────────────────────────────
  it("production check still passes", { timeout: 180000 }, () => {
    const result = spawnSync("bash", [join(ROOT, "scripts", "production-check.sh")], {
      cwd: ROOT,
      encoding: "utf8",
      timeout: 170000,
      maxBuffer: 2 * 1024 * 1024,
    });
    
    assert.equal(result.status, 0,
      `Production check failed (exit ${result.status})`);
  });
});
