import assert from "node:assert/strict";
import http from "node:http";
import test from "node:test";

import { createMcpAdapter } from "./mcp-adapter.js";
import type { Message } from "./types.js";

test("MCP adapter reset isolates later searches from prior run sessions", async () => {
  const stored = new Map<string, Message[]>();
  const server = http.createServer(async (req, res) => {
    if (req.url === "/health") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ ok: true }));
      return;
    }
    if (req.url !== "/rpc" || req.method !== "POST") {
      res.writeHead(404);
      res.end();
      return;
    }

    const body = await readRequestJson(req);
    const params = (body.params ?? {}) as Record<string, unknown>;
    let result: unknown = null;

    if (body.method === "engram.lcm.observe") {
      const sessionId = String(params.sessionId ?? "");
      stored.set(sessionId, params.messages as Message[]);
      result = { accepted: (params.messages as Message[]).length };
    } else if (body.method === "engram.lcm.search") {
      const query = String(params.query ?? "");
      const limit =
        typeof params.limit === "number" && Number.isFinite(params.limit)
          ? Math.max(1, Math.floor(params.limit))
          : 10;
      const sessionId =
        typeof params.sessionId === "string" ? params.sessionId : undefined;
      const sessionPrefix =
        typeof params.sessionPrefix === "string" ? params.sessionPrefix : undefined;
      const rows = [...stored.entries()]
        .filter(([storedSessionId]) =>
          sessionId
            ? storedSessionId === sessionId
            : !sessionPrefix || storedSessionId.startsWith(sessionPrefix),
        )
        .flatMap(([storedSessionId, messages]) =>
          messages
            .filter((message) => message.content.includes(query))
            .map((message, index) => ({
              turn_index: index,
              role: message.role,
              snippet: message.content,
              session_id: storedSessionId,
            })),
        );
      result = rows.slice(0, limit);
    } else if (body.method === "engram.lcm.recall") {
      const sessionId = String(params.sessionId ?? "");
      const query = String(params.query ?? "");
      result = (stored.get(sessionId) ?? [])
        .filter((message) => message.content.includes(query))
        .map((message) => message.content)
        .join("\n");
    } else if (body.method === "engram.lcm.stats") {
      result = statsForStoredSessions(stored, params);
    }

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id ?? 1, result }));
  });

  try {
    const baseUrl = await listen(server);
    const adapter = await createMcpAdapter({ baseUrl });

    await adapter.store("shared-session", [
      { role: "user", content: "old unique marker" },
    ]);
    assert.equal((await adapter.search("old unique marker", 10)).length, 1);
    assert.equal((await adapter.getStats()).totalMessages, 1);

    await adapter.reset();
    assert.deepEqual(await adapter.search("old unique marker", 10), []);
    assert.equal(await adapter.recall("shared-session", "old unique marker"), "");
    assert.equal((await adapter.getStats()).totalMessages, 0);

    await adapter.store("shared-session", [
      { role: "user", content: "new unique marker" },
    ]);
    const currentResults = await adapter.search("new unique marker", 10);
    assert.equal(currentResults.length, 1);
    assert.equal(currentResults[0]?.sessionId, "shared-session");
    assert.equal((await adapter.getStats()).totalMessages, 1);
    assert.equal((await adapter.getStats("shared-session")).totalMessages, 1);

    await adapter.destroy();
  } finally {
    await close(server);
  }
});

test("MCP adapter scopes global search to current run sessions before limiting", async () => {
  const stored = new Map<string, Message[]>();
  const requestedSessionIds: string[] = [];
  const server = http.createServer(async (req, res) => {
    if (req.url === "/health") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ ok: true }));
      return;
    }
    if (req.url !== "/rpc" || req.method !== "POST") {
      res.writeHead(404);
      res.end();
      return;
    }

    const body = await readRequestJson(req);
    const params = (body.params ?? {}) as Record<string, unknown>;
    let result: unknown = null;

    if (body.method === "engram.lcm.observe") {
      const sessionId = String(params.sessionId ?? "");
      stored.set(sessionId, params.messages as Message[]);
      result = { accepted: (params.messages as Message[]).length };
    } else if (body.method === "engram.lcm.search") {
      const query = String(params.query ?? "");
      const limit =
        typeof params.limit === "number" && Number.isFinite(params.limit)
          ? Math.max(1, Math.floor(params.limit))
          : 10;
      const sessionId =
        typeof params.sessionId === "string" ? params.sessionId : undefined;
      const sessionPrefix =
        typeof params.sessionPrefix === "string" ? params.sessionPrefix : undefined;
      requestedSessionIds.push(sessionId ?? sessionPrefix ?? "");
      const rows = [...stored.entries()]
        .filter(([storedSessionId]) =>
          sessionId
            ? storedSessionId === sessionId
            : !sessionPrefix || storedSessionId.startsWith(sessionPrefix),
        )
        .flatMap(([storedSessionId, messages]) =>
          messages
            .filter((message) => message.content.includes(query))
            .map((message, index) => ({
              turn_index: index,
              role: message.role,
              snippet: message.content,
              session_id: storedSessionId,
            })),
        );
      result = rows.slice(0, limit);
    } else if (body.method === "engram.lcm.recall") {
      result = "";
    } else if (body.method === "engram.lcm.stats") {
      result = { totalMessages: 0, totalSummaryNodes: 0, maxDepth: 0 };
    }

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id ?? 1, result }));
  });

  try {
    const baseUrl = await listen(server);
    const adapter = await createMcpAdapter({ baseUrl });

    for (let i = 0; i < 150; i += 1) {
      await adapter.store(`old-${i}`, [
        { role: "user", content: "shared marker from old run" },
      ]);
    }
    await adapter.reset();
    await adapter.store("current", [
      { role: "user", content: "shared marker from current run" },
    ]);

    const results = await adapter.search("shared marker", 10);

    assert.equal(results.length, 1);
    assert.equal(results[0]?.sessionId, "current");
    assert.ok(requestedSessionIds.every((sessionId) => sessionId !== ""));
    assert.ok(requestedSessionIds.some((sessionId) => sessionId.startsWith("eval-")));

    await adapter.destroy();
  } finally {
    await close(server);
  }
});

test("MCP adapter preserves zero-result search requests", async () => {
  let searchRequests = 0;
  const server = http.createServer(async (req, res) => {
    if (req.url === "/health") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ ok: true }));
      return;
    }
    if (req.url !== "/rpc" || req.method !== "POST") {
      res.writeHead(404);
      res.end();
      return;
    }

    const body = await readRequestJson(req);
    let result: unknown = null;

    if (body.method === "engram.lcm.search") {
      searchRequests += 1;
      result = [
        {
          turn_index: 0,
          role: "user",
          snippet: "unexpected result",
          session_id: "eval-unscoped:session",
        },
      ];
    } else if (body.method === "engram.lcm.observe") {
      result = { accepted: 1 };
    } else if (body.method === "engram.lcm.recall") {
      result = "";
    } else if (body.method === "engram.lcm.stats") {
      result = { totalMessages: 0, totalSummaryNodes: 0, maxDepth: 0 };
    }

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id ?? 1, result }));
  });

  try {
    const baseUrl = await listen(server);
    const adapter = await createMcpAdapter({ baseUrl });

    assert.deepEqual(await adapter.search("disabled retrieval", 0), []);
    assert.equal(searchRequests, 0);

    await adapter.destroy();
  } finally {
    await close(server);
  }
});

test("MCP adapter keeps in-flight search bound to its request run prefix", async () => {
  const stored = new Map<string, Message[]>();
  const releaseSearch = createDeferredForTest();
  const searchStarted = createDeferredForTest();
  const server = http.createServer(async (req, res) => {
    if (req.url === "/health") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ ok: true }));
      return;
    }
    if (req.url !== "/rpc" || req.method !== "POST") {
      res.writeHead(404);
      res.end();
      return;
    }

    const body = await readRequestJson(req);
    const params = (body.params ?? {}) as Record<string, unknown>;
    let result: unknown = null;

    if (body.method === "engram.lcm.observe") {
      stored.set(String(params.sessionId ?? ""), params.messages as Message[]);
      result = { accepted: (params.messages as Message[]).length };
    } else if (body.method === "engram.lcm.search") {
      const sessionPrefix =
        typeof params.sessionPrefix === "string" ? params.sessionPrefix : "";
      searchStarted.resolve();
      await releaseSearch.promise;
      result = [...stored.entries()]
        .filter(([sessionId]) => sessionId.startsWith(sessionPrefix))
        .flatMap(([sessionId, messages]) =>
          messages.map((message, index) => ({
            turn_index: index,
            role: message.role,
            snippet: message.content,
            session_id: sessionId,
          })),
        );
    } else if (body.method === "engram.lcm.recall") {
      result = "";
    } else if (body.method === "engram.lcm.stats") {
      result = { totalMessages: 0, totalSummaryNodes: 0, maxDepth: 0 };
    }

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id ?? 1, result }));
  });

  try {
    const baseUrl = await listen(server);
    const adapter = await createMcpAdapter({ baseUrl });

    await adapter.store("current", [
      { role: "user", content: "delayed search marker" },
    ]);
    const search = adapter.search("delayed search marker", 10);
    await searchStarted.promise;
    await adapter.reset();
    releaseSearch.resolve();

    const results = await search;
    assert.equal(results.length, 1);
    assert.equal(results[0]?.sessionId, "current");

    await adapter.destroy();
  } finally {
    await close(server);
  }
});

async function readRequestJson(req: http.IncomingMessage): Promise<{
  id?: unknown;
  method?: string;
  params?: unknown;
}> {
  let raw = "";
  for await (const chunk of req) {
    raw += String(chunk);
  }
  return JSON.parse(raw) as { id?: unknown; method?: string; params?: unknown };
}

function createDeferredForTest(): {
  promise: Promise<void>;
  resolve: () => void;
} {
  let resolve!: () => void;
  const promise = new Promise<void>((r) => {
    resolve = r;
  });
  return { promise, resolve };
}

function statsForStoredSessions(
  stored: Map<string, Message[]>,
  params: Record<string, unknown>,
): { totalMessages: number; totalSummaryNodes: number; maxDepth: number } {
  const sessionId =
    typeof params.sessionId === "string" ? params.sessionId : undefined;
  const totalMessages = [...stored.entries()]
    .filter(([storedSessionId]) =>
      !sessionId || storedSessionId === sessionId,
    )
    .reduce((count, [, messages]) => count + messages.length, 0);
  return {
    totalMessages,
    totalSummaryNodes: 0,
    maxDepth: totalMessages > 0 ? 0 : -1,
  };
}

async function listen(server: http.Server): Promise<string> {
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
  const address = server.address();
  assert.ok(address && typeof address === "object");
  return `http://127.0.0.1:${address.port}`;
}

async function close(server: http.Server): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    server.close((error) => {
      if (error) reject(error);
      else resolve();
    });
  });
}
