import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createRandomId } from "@best-agent/foundation";
import type { Item } from "@best-agent/protocol";
import { BestAgentError } from "@best-agent/foundation";
import { LocalProjectMemoryStore } from "./project-memory.js";

const roots: string[] = [];

afterEach(() => {
  while (roots.length > 0) {
    rmSync(roots.pop() as string, { force: true, recursive: true });
  }
});

describe("project memory store", () => {
  it("keeps manual memory in memory.md and exposes auto memory separately", () => {
    const { projectRoot, storageRoot } = createWorkspace("manual");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });

    const record = store.set("Repository uses pnpm.");

    expect(record.content).toContain("Repository uses pnpm.");
    expect(record.exists).toBe(true);
    expect(record.auto?.exists).toBe(false);
    expect(record.renderedContent).toContain("Manual memory");
  });

  it("promotes explicit durable user rules on the next session without writing them into manual memory", () => {
    const { projectRoot, storageRoot } = createWorkspace("user-rule");
    const first = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    first.captureCandidate({
      items: [userMessage("Always keep changes small and focused.")],
      sourceSessionId: "turn_memory_rule_1",
    });

    const second = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    second.prepareForSession();
    const record = second.get();

    expect(record.content).toBe("");
    expect(record.auto?.entryCount).toBe(1);
    expect(record.auto?.renderedContent).toContain("Always keep changes small and focused.");
    expect(record.renderedContent).toContain("Auto memory");
  });

  it("requires repeated successful workspace command observations before promoting command memory", () => {
    const { projectRoot, storageRoot } = createWorkspace("command-promotion");
    const first = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    first.captureCandidate({
      items: shellTurn("pnpm test"),
      sourceSessionId: "turn_memory_command_1",
    });

    const second = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    second.prepareForSession();
    expect(second.get().auto?.entryCount).toBe(0);

    second.captureCandidate({
      items: shellTurn("pnpm build"),
      sourceSessionId: "turn_memory_command_2",
    });

    const third = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    third.prepareForSession();
    expect(third.get().auto?.entryCount).toBe(1);
    expect(third.get().auto?.renderedContent).toContain("Use `pnpm` for workspace commands");
  });

  it("does not retrieve irrelevant workspace-command memory for an unrelated greeting", () => {
    const { projectRoot, storageRoot } = createWorkspace("greeting");
    seedPromotedWorkspaceCommand(projectRoot, storageRoot, "pnpm");

    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    const selection = store.buildPromptSelection({
      items: [userMessage("你好")],
    });

    expect(selection.prompt ?? "").not.toContain("Use `pnpm` for workspace commands");
    expect(selection.referencedEntryKeys).toEqual([]);
  });

  it("retrieves relevant workspace-command memory for command-oriented turns", () => {
    const { projectRoot, storageRoot } = createWorkspace("command-retrieval");
    seedPromotedWorkspaceCommand(projectRoot, storageRoot, "pnpm");

    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    const selection = store.buildPromptSelection({
      items: [userMessage("How should I run the repository tests?")],
    });

    expect(selection.prompt).toContain("Use `pnpm` for workspace commands");
    expect(selection.referencedEntryKeys).toEqual(["workspace-command:pnpm"]);
  });

  it("includes active workspace-command memory in the system prompt", () => {
    const { projectRoot, storageRoot } = createWorkspace("system-command-memory");
    seedPromotedWorkspaceCommand(projectRoot, storageRoot, "pnpm");

    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });

    expect(store.buildSystemPrompt()).toContain("Use `pnpm` for workspace commands");
  });

  it("extracts durable user rules from earlier user messages in a batch", () => {
    const { projectRoot, storageRoot } = createWorkspace("all-user-rules");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.captureCandidate({
      items: [
        userMessage("Always prefer async/await in this repository."),
        userMessage("Can you inspect the failing tests?"),
      ],
      sourceSessionId: "turn_memory_all_user_rules",
    });

    const nextSession = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    nextSession.prepareForSession();

    expect(nextSession.get().auto?.renderedContent).toContain(
      "Always prefer async/await in this repository.",
    );
  });

  it("learns common python and node workspace commands", () => {
    const { projectRoot, storageRoot } = createWorkspace("common-workspace-commands");
    const first = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    first.captureCandidate({
      items: shellTurn("python -m pytest"),
      sourceSessionId: "turn_memory_python_command_1",
    });
    const second = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    second.prepareForSession();
    second.captureCandidate({
      items: shellTurn("python scripts/test.py"),
      sourceSessionId: "turn_memory_python_command_2",
    });
    const third = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    third.prepareForSession();
    third.captureCandidate({
      items: shellTurn("node scripts/test.js"),
      sourceSessionId: "turn_memory_node_command_1",
    });
    const fourth = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    fourth.prepareForSession();
    fourth.captureCandidate({
      items: shellTurn("node scripts/build.js"),
      sourceSessionId: "turn_memory_node_command_2",
    });

    const nextSession = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    nextSession.prepareForSession();
    const rendered = nextSession.get().auto?.renderedContent ?? "";

    expect(rendered).toContain("Use `python` for workspace commands");
    expect(rendered).toContain("Use `node` for workspace commands");
  });

  it("keeps newly captured auto-memory candidates out of the current session until the next session prepares", () => {
    const { projectRoot, storageRoot } = createWorkspace("session-stability");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.captureCandidate({
      items: [userMessage("Always keep changes small and focused.")],
      sourceSessionId: "turn_memory_same_session",
    });

    expect(
      store.buildPromptSelection({ items: [userMessage("What should I remember?")] }).prompt ?? "",
    ).not.toContain("Always keep changes small and focused.");

    const nextSession = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    nextSession.prepareForSession();

    expect(
      nextSession.buildPromptSelection({ items: [userMessage("What should I remember?")] })
        .prompt ?? "",
    ).toContain("Always keep changes small and focused.");
  });

  it("drops pending candidates when the user clears project memory", () => {
    const { projectRoot, storageRoot } = createWorkspace("clear");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.captureCandidate({
      items: [userMessage("Always keep changes small and focused.")],
      sourceSessionId: "turn_memory_clear",
    });
    store.set("Repository uses pnpm.");

    const candidateDir = join(dirname(store.get().path), "auto-candidates");
    expect(readdirSync(candidateDir)).toHaveLength(1);

    const cleared = store.clear();

    expect(cleared.content).toBe("");
    expect(cleared.auto?.exists).toBe(false);
    expect(existsSync(candidateDir)).toBe(false);
  });

  it("syncs only manual memory entries", () => {
    const { projectRoot, storageRoot } = createWorkspace("sync-manual-only");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.set("Repository uses pnpm.");
    store.captureCandidate({
      items: [userMessage("Always keep changes small and focused.")],
      sourceSessionId: "turn_memory_sync_rule",
    });
    const nextSession = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    nextSession.prepareForSession();

    expect(nextSession.listSyncEntries()).toEqual([
      expect.objectContaining({
        content: "Repository uses pnpm.",
        key: "manual",
      }),
    ]);
  });

  it("merges synced manual memory without importing remote auto memory entries", () => {
    const { projectRoot, storageRoot } = createWorkspace("sync-merge");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.set("Repository uses pnpm.");

    const merged = store.mergeSyncEntries([
      {
        checksum: "remote-manual",
        content: "Repository uses bun.",
        key: "manual",
        updatedAt: new Date(Date.now() + 1_000).toISOString(),
      },
      {
        checksum: "ignored-generated",
        content: "Always run tests before shipping.",
        key: "generated:tests",
        updatedAt: new Date(Date.now() + 1_000).toISOString(),
      },
    ]);

    expect(merged.changed).toBe(true);
    expect(merged.memory.content.trim()).toBe("Repository uses bun.");
    expect(merged.memory.auto?.entryCount).toBe(0);
  });

  it("drops secret-bearing auto candidates while keeping safe durable ones", () => {
    const { projectRoot, storageRoot } = createWorkspace("secret");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.captureCandidate({
      items: [userMessage("Always keep changes small and focused.")],
      sourceSessionId: "turn_memory_secret_safe",
    });
    store.captureCandidate({
      items: [userMessage("Always use OPENAI_API_KEY=sk-proj-abcdefghijklmnopqrstuvwxyz123456")],
      sourceSessionId: "turn_memory_secret_blocked",
    });

    const nextSession = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    nextSession.prepareForSession();

    expect(nextSession.get().auto?.renderedContent).toContain(
      "Always keep changes small and focused.",
    );
    expect(nextSession.get().auto?.renderedContent).not.toContain("OPENAI_API_KEY");
  });

  it("blocks manual memory writes that contain secrets", () => {
    const { projectRoot, storageRoot } = createWorkspace("manual-secret");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });

    expect(() => {
      store.set("OPENAI_API_KEY=sk-proj-abcdefghijklmnopqrstuvwxyz123456");
    }).toThrowError(BestAgentError);
  });

  it("caps the combined project-memory prompt budget", () => {
    const { projectRoot, storageRoot } = createWorkspace("prompt-budget");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
    store.set(`Repository uses pnpm.\n\n${"Keep changes small and focused. ".repeat(120)}`);

    const prompt = store.buildSystemPrompt() ?? "";

    expect(Buffer.byteLength(prompt, "utf8")).toBeLessThanOrEqual(2300);
    expect(prompt).toContain("Repository uses pnpm.");
    expect(prompt).toContain("[additional project memory omitted from prompt]");
  });

  it("updates auto-memory references without changing which entry is stored", () => {
    const { projectRoot, storageRoot } = createWorkspace("references");
    seedPromotedWorkspaceCommand(projectRoot, storageRoot, "pnpm");
    const store = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });

    const before = readFileSync(store.get().auto?.path ?? "", "utf8");
    store.markReferenced(["workspace-command:pnpm"], "2026-04-13T00:00:00.000Z");
    const after = readFileSync(store.get().auto?.path ?? "", "utf8");

    expect(after).toContain('"referenceCount": 1');
    expect(after).toContain('"lastReferencedAt": "2026-04-13T00:00:00.000Z"');
    expect(after).toContain("workspace-command:pnpm");
    expect(before).not.toBe(after);
  });
});

function createWorkspace(name: string): { projectRoot: string; storageRoot: string } {
  const projectRoot = mkdtempSync(`${tmpdir()}/best-agent-project-memory-${name}-`);
  roots.push(projectRoot);
  return {
    projectRoot,
    storageRoot: join(projectRoot, "state"),
  };
}

function baseItem(overrides: Partial<Item>): Item {
  return {
    createdAt: "2026-04-13T00:00:00.000Z",
    id: createRandomId("item"),
    threadId: "thread_memory" as never,
    turnId: "turn_memory" as never,
    ...overrides,
  } as Item;
}

function shellTurn(command: string): Item[] {
  const callId = createRandomId("item") as never;
  return [
    baseItem({
      callId,
      input: command,
      kind: "tool-call",
      toolName: "shell",
    }),
    baseItem({
      callId,
      kind: "tool-result",
      output: "ok",
      toolName: "shell",
    }),
  ];
}

function userMessage(text: string): Item {
  return baseItem({
    kind: "message",
    role: "user",
    text,
  });
}

function seedPromotedWorkspaceCommand(
  projectRoot: string,
  storageRoot: string,
  command: string,
): void {
  const first = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
  first.captureCandidate({
    items: shellTurn(`${command} test`),
    sourceSessionId: "turn_memory_seed_1",
  });
  const second = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
  second.prepareForSession();
  second.captureCandidate({
    items: shellTurn(`${command} build`),
    sourceSessionId: "turn_memory_seed_2",
  });
  const third = new LocalProjectMemoryStore({ cwd: projectRoot, storageRoot });
  third.prepareForSession();
}
