import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createSessionStore } from "./session-store.js";

test("saves and restores public session state without runtime data", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-"));
  const store = createSessionStore(directory);
  const session = {
    id: "session-1",
    accountId: "account-a",
    workspaceId: "workspace-a",
    projectLabel: "Project A",
    conversationTitle: "Original request",
    codexThreadId: "thread-1",
    threadRefs: [{ accountId: "account-a", threadId: "thread-1" }],
    status: "completed",
    createdAt: "2026-06-27T00:00:00.000Z",
    updatedAt: "2026-06-27T00:01:00.000Z",
    events: [{ id: "event-1", event: "session.output", data: { text: "hello" } }],
    approvals: new Map(),
    files: [{ path: "README.md", status: "modified" }],
    attachments: [{ id: "attachment-1", kind: "image", name: "photo.jpg", mimeType: "image/jpeg", path: "C:\\Private\\photo.jpg", createdAt: "2026-06-27T00:00:20.000Z" }],
    queuedMessages: [{ id: "queued-1", text: "Later", executionAccountId: "account-b", options: {}, attachments: [{ id: "attachment-1", kind: "image", name: "photo.jpg", mimeType: "image/jpeg", path: "C:\\Private\\photo.jpg" }], skills: [], createdAt: "2026-06-27T00:00:30.000Z" }],
    runtime: { secret: "must-not-persist" },
    clients: new Set(["must-not-persist"])
  };

  try {
    await store.save([session]);
    const restored = await store.load();
    const raw = await readFile(join(directory, "sessions.json"), "utf8");

    assert.equal(restored.length, 1);
    assert.equal(restored[0].codexThreadId, "thread-1");
    assert.equal(restored[0].projectLabel, "Project A");
    assert.equal(restored[0].conversationTitle, "Original request");
    assert.deepEqual(restored[0].threadRefs, session.threadRefs);
    assert.deepEqual(restored[0].files, session.files);
    assert.deepEqual(restored[0].attachments, [{
      id: "attachment-1",
      kind: "image",
      name: "photo.jpg",
      mimeType: "image/jpeg",
      createdAt: "2026-06-27T00:00:20.000Z"
    }]);
    assert.deepEqual(restored[0].queuedMessages[0].attachments, [{ id: "attachment-1" }]);
    assert.equal(restored[0].queuedMessages[0].executionAccountId, "account-b");
    assert.equal(raw.includes("C:\\\\Private"), false);
    assert.equal(raw.includes("must-not-persist"), false);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("redacts private paths and credentials from persisted public state", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-"));
  const store = createSessionStore(directory, {
    accounts: [{ id: "account-a", codexHome: "C:\\CodexHome\\account-a" }],
    workspaces: [{ id: "workspace-a", path: "C:\\Project\\SecretApp" }],
    projectsRoot: "C:\\Project"
  });
  const session = {
    id: "session-1",
    accountId: "account-a",
    workspaceId: "workspace-a",
    conversationTitle: "Open \\\\server\\private share\\secret.txt with Basic dXNlcjpwYXNz",
    status: "failed",
    createdAt: "2026-06-27T00:00:00.000Z",
    updatedAt: "2026-06-27T00:01:00.000Z",
    events: [{
      id: "event-1",
      event: "error",
      data: {
        message: "Failed in C:\\Project\\SecretApp with Bearer abc.def and sk-secret",
        password: "plain password"
      }
    }],
    approvals: new Map([["approval-1", {
      id: "approval-1",
      status: "pending",
      kind: "command",
      cwd: "C:\\CodexHome\\account-a",
      reason: "Need C:\\Project\\SecretApp and api_key=plain-secret",
      commandPreview: "type C:\\Project\\SecretApp\\.env"
    }]])
  };

  try {
    await store.save([session]);
    const raw = await readFile(join(directory, "sessions.json"), "utf8");

    assert.equal(raw.includes("C:\\\\Project"), false);
    assert.equal(raw.includes("C:\\\\CodexHome"), false);
    assert.equal(raw.includes("abc.def"), false);
    assert.equal(raw.includes("sk-secret"), false);
    assert.equal(raw.includes("plain-secret"), false);
    assert.equal(raw.includes("plain password"), false);
    assert.equal(raw.includes("dXNlcjpwYXNz"), false);
    assert.equal(raw.includes("private share"), false);
    assert.match(raw, /\[private-path\]/);
    assert.match(raw, /\[redacted\]/);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("restores durable command, tool, plan, and terminal error events", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-results-"));
  const store = createSessionStore(directory);
  const events = [
    { id: "plan", event: "plan.updated", data: { turnId: "turn-1", plan: [{ step: "Inspect", status: "completed" }] } },
    { id: "command", event: "activity.completed", data: { turnId: "turn-1", itemId: "command-1", kind: "commandExecution", text: "stdout", exitCode: 0 } },
    { id: "tool", event: "activity.completed", data: { turnId: "turn-1", itemId: "tool-1", kind: "mcpToolCall", text: "result" } },
    { id: "error", event: "error", data: { turnId: "turn-1", message: "usage_limit_reached" } }
  ];

  try {
    await store.save([{
      id: "session-results",
      status: "failed",
      createdAt: "2026-07-14T00:00:00.000Z",
      updatedAt: "2026-07-14T00:01:00.000Z",
      events,
      approvals: new Map()
    }]);
    assert.deepEqual((await store.load())[0].events, events);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("persists queued messages without credentials or executable resource paths", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-"));
  const store = createSessionStore(directory, {
    accounts: [{ id: "account-a", codexHome: "C:\\CodexHome\\account-a" }],
    workspaces: [{ id: "workspace-a", path: "C:\\Project\\SecretApp" }]
  });
  const session = {
    id: "session-queued-secret",
    accountId: "account-a",
    workspaceId: "workspace-a",
    status: "running",
    createdAt: "2026-06-27T00:00:00.000Z",
    updatedAt: "2026-06-27T00:01:00.000Z",
    events: [],
    approvals: new Map(),
    queuedMessages: [{
      id: "queued-1",
      text: "Use Bearer queued-token in C:\\Project\\SecretApp\\.env",
      options: {},
      attachments: [],
      skills: [{ name: "imagegen", path: "C:\\Skills\\imagegen" }],
      createdAt: "2026-06-27T00:00:30.000Z"
    }]
  };

  try {
    await store.save([session]);
    const raw = await readFile(join(directory, "sessions.json"), "utf8");
    const restored = JSON.parse(raw);

    assert.equal(raw.includes("queued-token"), false);
    assert.equal(raw.includes("C:\\\\Project\\\\SecretApp"), false);
    assert.equal(raw.includes("auth.json"), false);
    assert.equal(raw.includes("C:\\\\Skills"), false);
    assert.deepEqual(restored[0].queuedMessages[0].skills, [{ name: "imagegen" }]);
    assert.equal(restored[0].queuedMessages[0].text, "Use Bearer [redacted] in [private-path]\\.env");
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("returns an empty list when no session file exists", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-"));
  try {
    assert.deepEqual(await createSessionStore(directory).load(), []);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("reuses sanitized snapshots for unchanged sessions", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-cache-"));
  const store = createSessionStore(directory);
  const unchanged = {
    id: "unchanged",
    accountId: "account-a",
    workspaceId: "workspace-a",
    status: "idle",
    createdAt: "2026-07-10T00:00:00.000Z",
    updatedAt: "2026-07-10T00:00:00.000Z",
    events: []
  };
  const changed = { ...unchanged, id: "changed", events: [] };
  try {
    await store.save([unchanged, changed]);
    Object.defineProperty(unchanged, "events", {
      get() { throw new Error("unchanged session was re-sanitized"); }
    });
    changed.events.push({ id: "event-1", event: "session.status", data: { status: "running" } });

    await store.save([unchanged, changed], { dirtySessionIds: new Set(["changed"]) });

    const saved = JSON.parse(await readFile(join(directory, "sessions.json"), "utf8"));
    assert.equal(saved.find((session) => session.id === "unchanged").events.length, 0);
    assert.equal(saved.find((session) => session.id === "changed").events.length, 1);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("backs up a corrupt session file and recovers with an empty store", async () => {
  const directory = await mkdtemp(join(tmpdir(), "code-remote-manager-store-"));
  const filePath = join(directory, "sessions.json");
  const warnings = [];
  try {
    await writeFile(filePath, '{"broken":', "utf8");

    const restored = await createSessionStore(directory, {}, { warn: (message) => warnings.push(message) }).load();

    assert.deepEqual(restored, []);
    assert.equal(await readFile(filePath, "utf8"), "[]");
    assert.equal((await readdir(directory)).some((name) => /^sessions\.json\.broken\.\d+$/.test(name)), true);
    assert.deepEqual(warnings, [JSON.stringify({ level: "warn", code: "session_store_recovered" })]);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});
