import { describe, expect, it, vi } from "vitest";
import { createSnapshotOnTurnEnd } from "./rollback-hook.js";

describe("snapshot-on-turn-end hook", () => {
  it("commits a snapshot labeled with the last assistant message id", async () => {
    const rpc = {
      snapshotCommit: vi.fn(async () => ({ snapId: 3, committedAt: "t", label: "asst-99" })),
    };
    const sessionStore = {
      load: vi.fn(async () => ({ secafsRollback: { enabled: true } })),
      patch: vi.fn(async () => undefined),
    };
    const findLast = vi.fn(async () => "asst-99");
    const events = vi.fn();
    const hook = createSnapshotOnTurnEnd({
      rpc,
      sessionStore,
      findLastAssistantMessageId: findLast,
      events,
      sessionFileFor: () => "/p/s.jsonl",
      extractConvId: () => "vol",
    });
    await hook({ sessionKey: "main:vol" });
    expect(rpc.snapshotCommit).toHaveBeenCalledWith({ conversationId: "vol", label: "asst-99" });
    expect(events).toHaveBeenCalledWith(
      expect.objectContaining({ type: "secafs.rollback.snapshotCommitted", snapId: 3 }),
    );
  });

  it("skips when rollback is not enabled", async () => {
    const rpc = { snapshotCommit: vi.fn() };
    const sessionStore = {
      load: vi.fn(async () => ({ secafsRollback: { enabled: false } })),
      patch: vi.fn(),
    };
    const findLast = vi.fn(async () => "asst-99");
    const hook = createSnapshotOnTurnEnd({
      rpc,
      sessionStore,
      findLastAssistantMessageId: findLast,
      events: vi.fn(),
      sessionFileFor: () => "x",
      extractConvId: () => "v",
    });
    await hook({ sessionKey: "k" });
    expect(rpc.snapshotCommit).not.toHaveBeenCalled();
  });

  it("skips when last assistant message id is the same as last snapshot label", async () => {
    const rpc = { snapshotCommit: vi.fn() };
    const sessionStore = {
      load: vi.fn(async () => ({ secafsRollback: { enabled: true, lastSnapshotMessageId: "m1" } })),
      patch: vi.fn(),
    };
    const findLast = vi.fn(async () => "m1");
    const hook = createSnapshotOnTurnEnd({
      rpc,
      sessionStore,
      findLastAssistantMessageId: findLast,
      events: vi.fn(),
      sessionFileFor: () => "x",
      extractConvId: () => "v",
    });
    await hook({ sessionKey: "k" });
    expect(rpc.snapshotCommit).not.toHaveBeenCalled();
  });

  it("skips when no assistant message exists yet", async () => {
    const rpc = { snapshotCommit: vi.fn() };
    const sessionStore = {
      load: vi.fn(async () => ({ secafsRollback: { enabled: true } })),
      patch: vi.fn(),
    };
    const findLast = vi.fn(async () => null);
    const hook = createSnapshotOnTurnEnd({
      rpc,
      sessionStore,
      findLastAssistantMessageId: findLast,
      events: vi.fn(),
      sessionFileFor: () => "x",
      extractConvId: () => "v",
    });
    await hook({ sessionKey: "k" });
    expect(rpc.snapshotCommit).not.toHaveBeenCalled();
  });

  it("skips when restore is in progress", async () => {
    const rpc = { snapshotCommit: vi.fn() };
    const sessionStore = {
      load: vi.fn(async () => ({
        secafsRollback: {
          enabled: true,
          inProgress: {
            targetSnapId: 1,
            targetMessageId: "x",
            fsRestored: false,
            jsonlTruncated: false,
            startedAt: 0,
          },
        },
      })),
      patch: vi.fn(),
    };
    const findLast = vi.fn(async () => "asst-1");
    const hook = createSnapshotOnTurnEnd({
      rpc,
      sessionStore,
      findLastAssistantMessageId: findLast,
      events: vi.fn(),
      sessionFileFor: () => "x",
      extractConvId: () => "v",
    });
    await hook({ sessionKey: "k" });
    expect(rpc.snapshotCommit).not.toHaveBeenCalled();
  });
});

describe("findLastAssistantMessageId (real openclaw JSONL format)", () => {
  it("finds id from the openclaw nested {type:message, message:{role:assistant}} shape", async () => {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const os = await import("node:os");
    const { findLastAssistantMessageId } = await import("./rollback-hook.js");

    const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rollback-jsonl-"));
    const file = path.join(dir, "session.jsonl");
    const lines = [
      `{"type":"session","version":3,"id":"sess1","timestamp":"2026-01-01"}`,
      `{"type":"model_change","id":"m1","provider":"x"}`,
      `{"type":"message","id":"u-1","message":{"role":"user","content":[]}}`,
      `{"type":"message","id":"a-1","message":{"role":"assistant","content":[]}}`,
      `{"type":"message","id":"u-2","message":{"role":"user","content":[]}}`,
      `{"type":"message","id":"a-2","message":{"role":"assistant","content":[]}}`,
    ];
    await fs.writeFile(file, lines.join("\n") + "\n");
    const id = await findLastAssistantMessageId(file);
    expect(id).toBe("a-2");
    await fs.rm(dir, { recursive: true, force: true });
  });

  it("returns null when no assistant message exists yet (only user + meta events)", async () => {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const os = await import("node:os");
    const { findLastAssistantMessageId } = await import("./rollback-hook.js");

    const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rollback-jsonl-"));
    const file = path.join(dir, "session.jsonl");
    const lines = [
      `{"type":"session","version":3,"id":"sess1"}`,
      `{"type":"message","id":"u-1","message":{"role":"user","content":[]}}`,
    ];
    await fs.writeFile(file, lines.join("\n") + "\n");
    const id = await findLastAssistantMessageId(file);
    expect(id).toBeNull();
    await fs.rm(dir, { recursive: true, force: true });
  });
});