import { test } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import { MemoryManager, LocalMemoryProvider } from "./manager.ts";
import { WorkMemoryStore } from "./store.ts";
import { memoryTool } from "./tool.ts";

function testDb() {
  return new DatabaseSync(":memory:");
}

function testStore(options = {}) {
  return new WorkMemoryStore(testDb(), options);
}

function completeNextAiRequest(db: DatabaseSync, response: string) {
  const timer = setInterval(() => {
    const row = db.prepare("SELECT id FROM ai_requests WHERE status='pending' ORDER BY created_at ASC LIMIT 1").get() as { id: string } | undefined;
    if (!row) return;
    db.prepare("UPDATE ai_requests SET status='done', response=?, completed_at=? WHERE id=?").run(response, Date.now(), row.id);
    clearInterval(timer);
  }, 10);
}

test("memory tool add, read, replace, and remove", async () => {
  const store = testStore();

  const added = await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "DewLink memory must stay local-first in the existing SQLite database.",
  });
  assert.equal(added.success, true);
  assert.equal(added.message, "Entry added.");

  const read = await memoryTool(store, { action: "read", scope: "business" });
  assert.equal(read.success, true);
  assert.equal(read.entries?.length, 1);

  const replaced = await memoryTool(store, {
    action: "replace",
    scope: "business",
    old_text: "local-first",
    content: "DewLink memory must stay local-only in the existing SQLite database.",
  });
  assert.equal(replaced.success, true);
  assert.match(JSON.stringify(replaced.entries), /local-only/);

  const removed = await memoryTool(store, { action: "remove", scope: "business", old_text: "local-only" });
  assert.equal(removed.success, true);
  assert.equal(removed.entries?.length, 0);
});

test("replace and remove require a short unique substring", async () => {
  const store = testStore();
  await memoryTool(store, { action: "add", scope: "owner", content: "Roki prefers direct code review findings first." });
  await memoryTool(store, { action: "add", scope: "owner", content: "Roki prefers direct operator updates without fluff." });

  const ambiguous = await memoryTool(store, {
    action: "replace",
    scope: "owner",
    old_text: "Roki prefers direct",
    content: "Roki prefers concise updates.",
  });
  assert.equal(ambiguous.success, false);
  assert.match(ambiguous.error ?? "", /Multiple entries matched/);
  assert.equal(ambiguous.matches?.length, 2);
});

test("budget overflow triggers compaction and records an audit row", async () => {
  const db = testDb();
  const store = new WorkMemoryStore(db, { businessCharLimit: 150 });
  await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "Roki prefers direct code review findings first.",
  });
  completeNextAiRequest(db, [
    "Roki prefers direct code review findings first and direct operator updates without fluff.",
    "DewLink memory compaction must preserve unique facts.",
  ].join("\n"));

  const result = await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "Roki prefers direct operator updates without fluff. DewLink memory compaction must preserve unique facts.",
  });
  assert.equal(result.success, true);
  assert.equal(result.message, "Entry added and memory compacted.");
  assert.deepEqual(result.entries?.map((entry: any) => entry.body).sort(), [
    "DewLink memory compaction must preserve unique facts.",
    "Roki prefers direct code review findings first and direct operator updates without fluff.",
  ].sort());
  assert.deepEqual(store.compactions("business").map((row) => ({
    scope: row.scope,
    before_count: row.before_count,
    after_count: row.after_count,
  })), [{ scope: "business", before_count: 2, after_count: 2 }]);
});

test("malformed compaction output keeps original entries", async () => {
  const db = testDb();
  const store = new WorkMemoryStore(db, { businessCharLimit: 80 });
  await memoryTool(store, { action: "add", scope: "business", content: "First durable business fact stays intact." });
  completeNextAiRequest(db, '{"facts":["not one fact per line"]}');

  const result = await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "Second unique fact should remain even if compaction output is malformed.",
  });
  assert.equal(result.success, true);
  assert.equal(result.message, "Entry added.");
  assert.equal(result.entries?.length, 2);
  assert.match(JSON.stringify(result.entries), /First durable business fact/);
  assert.match(JSON.stringify(result.entries), /Second unique fact/);
  assert.equal(store.compactions("business").length, 0);
});

test("under-budget insert never compacts", async () => {
  const db = testDb();
  const store = new WorkMemoryStore(db, { businessCharLimit: 500 });
  const result = await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "Small durable business fact.",
  });
  assert.equal(result.success, true);
  assert.equal(result.message, "Entry added.");
  const pending = db.prepare("SELECT COUNT(*) AS c FROM ai_requests").get() as { c: number };
  assert.equal(pending.c, 0);
  assert.equal(store.compactions("business").length, 0);
});

test("FTS search finds agent messages and memory entries", async () => {
  const store = testStore();
  const session = store.createSession({ id: "session:fts", source: "test", model: "codex" }) as any;
  store.addMessage({
    session_id: session.id,
    role: "assistant",
    content: "Hermes memory round trip should find this alphaomega phrase.",
  });
  await memoryTool(store, {
    action: "add",
    scope: "business",
    content: "The durable memory search should find betaomega evidence.",
  });

  const messageHits = store.search("alphaomega");
  assert.equal(messageHits.some((hit) => hit.kind === "message" && String(hit.session_id) === "session:fts"), true);

  const memoryHits = store.search("betaomega");
  assert.equal(memoryHits.some((hit) => hit.kind === "memory" && hit.scope === "business"), true);
});

test("contact memory does not leak into business snapshot and snapshots stay frozen", async () => {
  const store = testStore();
  await memoryTool(store, { action: "add", scope: "business", content: "Business fact: use LocalDB work tables." });
  await memoryTool(store, { action: "add", scope: "contact:abc123", content: "Contact fact: Fatima needs Friday follow-up." });

  const manager = new MemoryManager(new LocalMemoryProvider(store));
  const businessSnapshot = manager.getSnapshot(["business"]);
  assert.match(businessSnapshot, /Business fact/);
  assert.doesNotMatch(businessSnapshot, /Fatima/);

  await memoryTool(store, { action: "add", scope: "business", content: "Business fact: added mid-session." });
  const stillFrozen = manager.getSnapshot(["business"]);
  assert.doesNotMatch(stillFrozen, /mid-session/);

  const nextSession = new MemoryManager(new LocalMemoryProvider(store));
  assert.match(nextSession.getSnapshot(["business"]), /mid-session/);
});
