// Covers plugin-backed memory state registration and reset behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  buildMemoryPromptSection,
  clearMemoryPluginState,
  getMemoryCapabilityRegistration,
  getMemoryRuntime,
  listMemoryCorpusSupplements,
  listMemoryPromptPreparations,
  listActiveMemoryPublicArtifacts,
  prepareMemoryPromptSection,
  registerMemoryCapability,
  registerMemoryCorpusSupplement,
  registerMemoryPromptPreparation,
  registerMemoryPromptSupplement,
  registerTestMemoryPromptBuilder,
  resolveMemoryFlushPlan,
  type MemoryPluginPublicArtifact,
} from "./memory-state.test-fixtures.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import { withPluginRegistrationContext } from "./runtime.js";

function createMemoryRuntime() {
  return {
    async getMemorySearchManager() {
      return { manager: null, error: "missing" };
    },
    resolveMemoryBackendConfig() {
      return { backend: "builtin" as const };
    },
  };
}

function createMemoryFlushPlan(relativePath: string) {
  return {
    softThresholdTokens: 1,
    forceFlushTranscriptBytes: 2,
    reserveTokensFloor: 3,
    prompt: relativePath,
    systemPrompt: relativePath,
    relativePath,
  };
}

function expectClearedMemoryState() {
  expect(resolveMemoryFlushPlan({})).toBeNull();
  expect(buildMemoryPromptSection({ availableTools: new Set(["memory_search"]) })).toStrictEqual(
    [],
  );
  expect(listMemoryCorpusSupplements()).toStrictEqual([]);
  expect(getMemoryRuntime()).toBeUndefined();
}

function registerMemoryState(params: {
  promptSection?: string[];
  relativePath?: string;
  runtime?: ReturnType<typeof createMemoryRuntime>;
}) {
  registerMemoryCapability("memory-core", {
    ...(params.promptSection ? { promptBuilder: () => params.promptSection ?? [] } : {}),
    ...(params.relativePath
      ? { flushPlanResolver: () => createMemoryFlushPlan(params.relativePath ?? "") }
      : {}),
    ...(params.runtime ? { runtime: params.runtime } : {}),
  });
}

describe("memory plugin state", () => {
  afterEach(() => {
    clearMemoryPluginState();
  });

  it("returns empty defaults when no memory plugin state is registered", () => {
    expectClearedMemoryState();
  });

  it("attributes direct builder registrations to the synchronous plugin owner", () => {
    const building = createEmptyPluginRegistry();

    withPluginRegistrationContext(building, "actual-plugin", () => {
      registerMemoryCapability("spoofed-plugin", { runtime: createMemoryRuntime() });
      registerMemoryCorpusSupplement("spoofed-plugin", {
        search: async () => [],
        get: async () => null,
      });
      registerMemoryPromptSupplement("spoofed-plugin", () => ["supplement"]);
      registerMemoryPromptPreparation("spoofed-plugin", async () => ["prepared"]);
    });

    expect(building.memoryCapabilities[0]?.pluginId).toBe("actual-plugin");
    expect(building.memoryCorpusSupplements[0]?.pluginId).toBe("actual-plugin");
    expect(building.memoryPromptSupplements[0]?.pluginId).toBe("actual-plugin");
    expect(building.memoryPromptPreparations[0]?.pluginId).toBe("actual-plugin");
  });

  it("delegates prompt building to the registered memory plugin", () => {
    registerTestMemoryPromptBuilder(({ availableTools }) => {
      if (!availableTools.has("memory_search")) {
        return [];
      }
      return ["## Custom Memory", "Use custom memory tools.", ""];
    });

    expect(buildMemoryPromptSection({ availableTools: new Set(["memory_search"]) })).toEqual([
      "## Custom Memory",
      "Use custom memory tools.",
      "",
    ]);
  });

  it("lists active public memory artifacts in deterministic order", async () => {
    registerMemoryCapability("memory-core", {
      publicArtifacts: {
        async listArtifacts() {
          return [
            {
              kind: "daily-note",
              workspaceDir: "/tmp/workspace-b",
              relativePath: "memory/2026-04-06.md",
              absolutePath: "/tmp/workspace-b/memory/2026-04-06.md",
              agentIds: ["beta"],
              contentType: "markdown" as const,
            },
            {
              kind: "memory-root",
              workspaceDir: "/tmp/workspace-a",
              relativePath: "MEMORY.md",
              absolutePath: "/tmp/workspace-a/MEMORY.md",
              agentIds: ["main"],
              contentType: "markdown" as const,
            },
          ];
        },
      },
    });

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
      {
        kind: "memory-root",
        workspaceDir: "/tmp/workspace-a",
        relativePath: "MEMORY.md",
        absolutePath: "/tmp/workspace-a/MEMORY.md",
        agentIds: ["main"],
        contentType: "markdown",
      },
      {
        kind: "daily-note",
        workspaceDir: "/tmp/workspace-b",
        relativePath: "memory/2026-04-06.md",
        absolutePath: "/tmp/workspace-b/memory/2026-04-06.md",
        agentIds: ["beta"],
        contentType: "markdown",
      },
    ]);
  });

  it("normalizes public memory artifacts without agent ids", async () => {
    const legacyArtifact = {
      kind: "memory-root",
      workspaceDir: "/tmp/workspace",
      relativePath: "MEMORY.md",
      absolutePath: "/tmp/workspace/MEMORY.md",
      contentType: "markdown" as const,
    } as Omit<MemoryPluginPublicArtifact, "agentIds"> as MemoryPluginPublicArtifact;

    registerMemoryCapability("memory-core", {
      publicArtifacts: {
        async listArtifacts() {
          return [legacyArtifact];
        },
      },
    });

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
      {
        kind: "memory-root",
        workspaceDir: "/tmp/workspace",
        relativePath: "MEMORY.md",
        absolutePath: "/tmp/workspace/MEMORY.md",
        agentIds: [],
        contentType: "markdown",
      },
    ]);
  });

  it("drops malformed public memory artifacts instead of crashing the sort", async () => {
    // Record-shaped artifact as shipped by @mem0/openclaw-mem0 <= 1.0.14 —
    // none of the file-backed fields the sort dereferences.
    const recordShapedArtifact = {
      id: "mem0:memory:1",
      type: "memory",
      title: "A memory",
      content: "memory text",
    } as unknown as MemoryPluginPublicArtifact;

    registerMemoryCapability("openclaw-mem0", {
      publicArtifacts: {
        async listArtifacts() {
          return [
            recordShapedArtifact,
            {
              kind: "memory-root",
              workspaceDir: "/tmp/workspace",
              relativePath: "MEMORY.md",
              absolutePath: "/tmp/workspace/MEMORY.md",
              agentIds: ["main"],
              contentType: "markdown" as const,
            },
          ];
        },
      },
    });

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
      {
        kind: "memory-root",
        workspaceDir: "/tmp/workspace",
        relativePath: "MEMORY.md",
        absolutePath: "/tmp/workspace/MEMORY.md",
        agentIds: ["main"],
        contentType: "markdown",
      },
    ]);
  });

  it("ignores a non-array public artifact listing", async () => {
    registerMemoryCapability("openclaw-mem0", {
      publicArtifacts: {
        async listArtifacts() {
          return { artifacts: [] } as unknown as MemoryPluginPublicArtifact[];
        },
      },
    });

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([]);
  });

  it("preserves sidecar runtime fields when a memory plugin adds public artifacts only", async () => {
    const runtime = createMemoryRuntime();
    const flushPlanResolver = () => createMemoryFlushPlan("memory/sidecar.md");

    registerMemoryCapability("memory-core", {
      flushPlanResolver,
      runtime,
    });
    registerMemoryCapability("memory-lancedb", {
      publicArtifacts: {
        async listArtifacts() {
          return [
            {
              kind: "memory-root",
              workspaceDir: "/tmp/workspace",
              relativePath: "MEMORY.md",
              absolutePath: "/tmp/workspace/MEMORY.md",
              agentIds: ["main"],
              contentType: "markdown" as const,
            },
          ];
        },
      },
    });

    expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/sidecar.md");
    expect(getMemoryRuntime()).toBe(runtime);
    expect(getMemoryCapabilityRegistration()?.pluginId).toBe("memory-lancedb");
    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
      {
        kind: "memory-root",
        workspaceDir: "/tmp/workspace",
        relativePath: "MEMORY.md",
        absolutePath: "/tmp/workspace/MEMORY.md",
        agentIds: ["main"],
        contentType: "markdown",
      },
    ]);
  });

  it("preserves runtime fields when the same plugin adds public artifacts", () => {
    const runtime = createMemoryRuntime();
    const flushPlanResolver = () => createMemoryFlushPlan("memory/same-owner.md");

    registerMemoryCapability("memory-core", { runtime, flushPlanResolver });
    registerMemoryCapability("memory-core", {
      publicArtifacts: { listArtifacts: async () => [] },
    });

    expect(getMemoryRuntime()).toBe(runtime);
    expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/same-owner.md");
  });

  it("passes citations mode through to the prompt builder", () => {
    registerTestMemoryPromptBuilder(({ citationsMode }) => [
      `citations: ${citationsMode ?? "default"}`,
    ]);

    expect(
      buildMemoryPromptSection({
        availableTools: new Set(),
        citationsMode: "off",
      }),
    ).toEqual(["citations: off"]);
  });

  it("passes agent context through the primary and supplemental prompt builders", () => {
    const primary = vi.fn(() => ["primary"]);
    const supplemental = vi.fn(() => ["supplemental"]);
    registerTestMemoryPromptBuilder(primary);
    registerMemoryPromptSupplement("memory-wiki", supplemental);

    const availableTools = new Set(["memory_search", "memory_get"]);
    expect(
      buildMemoryPromptSection({
        availableTools,
        citationsMode: "on",
        agentId: "marketing-agent",
        agentSessionKey: "agent:marketing-agent:main",
        sandboxed: true,
      }),
    ).toEqual(["primary", "supplemental"]);
    const expectedContext = {
      availableTools,
      citationsMode: "on",
      agentId: "marketing-agent",
      agentSessionKey: "agent:marketing-agent:main",
      sandboxed: true,
    };
    expect(primary).toHaveBeenCalledWith(expectedContext);
    expect(supplemental).toHaveBeenCalledWith(expectedContext);
  });

  it("appends prompt supplements in plugin-id order", () => {
    registerTestMemoryPromptBuilder(() => ["primary"]);
    registerMemoryPromptSupplement("memory-wiki", () => ["wiki"]);
    registerMemoryPromptSupplement("alpha-helper", () => ["alpha"]);

    expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
      "primary",
      "alpha",
      "wiki",
    ]);
  });

  it("ignores malformed prompt builder output", () => {
    registerTestMemoryPromptBuilder(() => ["primary", 1, undefined] as never);
    registerMemoryPromptSupplement("async-helper", () => Promise.resolve(["async"]) as never);
    registerMemoryPromptSupplement("valid-helper", () => ["valid", false] as never);

    expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual(["primary", "valid"]);
  });

  it("prepares immutable prompt lines once per run before synchronous assembly", async () => {
    let compiledLines = ["compiled before"];
    const prepare = vi.fn(async () => [...compiledLines]);
    registerMemoryPromptPreparation("memory-wiki", prepare);
    const params = {
      availableTools: new Set(["wiki_search"]),
      agentId: "main",
      agentSessionKey: "agent:main:main",
    };

    const preparedBefore = await prepareMemoryPromptSection(params);
    compiledLines = ["compiled after"];

    expect(Object.isFrozen(preparedBefore)).toBe(true);
    expect(Object.isFrozen(preparedBefore.context)).toBe(true);
    expect(Object.isFrozen(preparedBefore.context.availableTools)).toBe(true);
    expect(Object.isFrozen(preparedBefore.lines)).toBe(true);
    expect(buildMemoryPromptSection(params, preparedBefore)).toEqual(["compiled before"]);
    expect(buildMemoryPromptSection(params, preparedBefore)).toEqual(["compiled before"]);
    expect(prepare).toHaveBeenCalledTimes(1);

    const preparedAfter = await prepareMemoryPromptSection(params);
    expect(buildMemoryPromptSection(params, preparedAfter)).toEqual(["compiled after"]);
    expect(prepare).toHaveBeenCalledTimes(2);
  });

  it("rejects prepared state from a different run context", async () => {
    registerMemoryPromptPreparation("memory-wiki", async () => ["private wiki state"]);
    const prepared = await prepareMemoryPromptSection({
      availableTools: new Set(["wiki_search"]),
      agentId: "first",
      agentSessionKey: "agent:first:main",
    });

    expect(() =>
      buildMemoryPromptSection(
        {
          availableTools: new Set(["wiki_search"]),
          agentId: "second",
          agentSessionKey: "agent:second:main",
        },
        prepared,
      ),
    ).toThrow("prepared memory prompt section does not match the current run");
  });

  it("removes prompt preparations from future runs without mutating an in-flight snapshot", async () => {
    registerMemoryPromptPreparation("memory-wiki", async () => ["prepared wiki"]);
    const params = { availableTools: new Set<string>() };
    const prepared = await prepareMemoryPromptSection(params);

    clearMemoryPluginState();

    const afterRemoval = await prepareMemoryPromptSection(params);
    expect(buildMemoryPromptSection(params, prepared)).toEqual(["prepared wiki"]);
    expect(buildMemoryPromptSection(params, afterRemoval)).toEqual([]);
    expect(listMemoryPromptPreparations()).toEqual([]);
  });

  it("stores memory corpus supplements", async () => {
    const supplement = {
      search: async () => [{ corpus: "wiki", path: "sources/alpha.md", score: 1, snippet: "x" }],
      get: async () => null,
    };

    registerMemoryCorpusSupplement("memory-wiki", supplement);

    expect(listMemoryCorpusSupplements()).toHaveLength(1);
    await expect(
      listMemoryCorpusSupplements()[0]?.supplement.search({ query: "alpha" }),
    ).resolves.toEqual([{ corpus: "wiki", path: "sources/alpha.md", score: 1, snippet: "x" }]);
  });

  it("clearMemoryPluginState resets both registries", () => {
    registerMemoryState({
      promptSection: ["stale section"],
      relativePath: "memory/stale.md",
      runtime: createMemoryRuntime(),
    });

    clearMemoryPluginState();

    expectClearedMemoryState();
  });
});
