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

test("persists and replays a message receipt without storing the client key or payload", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-action-receipts-"));
  const clientMessageId = "11111111-1111-4111-8111-111111111111";
  try {
    const firstStore = createActionReceiptStore(directory);
    await firstStore.initialize();
    const first = await firstStore.reserveMessage({
      sessionId: "session-1",
      clientMessageId,
      payloadHash: "a".repeat(64),
      delivery: { disposition: "started" },
      state: "accepted"
    });

    const secondStore = createActionReceiptStore(directory);
    await secondStore.initialize();
    const replay = await secondStore.findMessage({
      sessionId: "session-1",
      clientMessageId,
      payloadHash: "a".repeat(64)
    });
    const raw = await readFile(join(directory, "action-receipts.json"), "utf8");

    assert.equal(first.created, true);
    assert.deepEqual(replay.delivery, { disposition: "started" });
    assert.equal(raw.includes(clientMessageId), false);
    assert.equal(raw.includes("session-1"), false);
    assert.equal(raw.includes("message body"), false);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("rejects a reused message key with a different payload hash", async () => {
  const store = createActionReceiptStore();
  await store.initialize();
  const input = {
    sessionId: "session-1",
    clientMessageId: "22222222-2222-4222-8222-222222222222",
    payloadHash: "a".repeat(64),
    delivery: { disposition: "started" },
    state: "accepted"
  };
  await store.reserveMessage(input);

  await assert.rejects(
    store.findMessage({ ...input, payloadHash: "b".repeat(64) }),
    (error) => error.message === "idempotency_conflict" && error.status === 409
  );
});

test("fails closed instead of replacing a corrupt action receipt ledger", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-action-receipts-"));
  try {
    await writeFile(join(directory, "action-receipts.json"), "{broken", "utf8");
    const store = createActionReceiptStore(directory);
    await store.initialize();

    await assert.rejects(
      store.reserveMessage({
        sessionId: "session-1",
        clientMessageId: "33333333-3333-4333-8333-333333333333",
        payloadHash: "a".repeat(64),
        delivery: { disposition: "started" },
        state: "accepted"
      }),
      (error) => error.message === "idempotency_store_unavailable" && error.safeLogCode === "idempotency_store_unavailable"
    );
    assert.equal(await readFile(join(directory, "action-receipts.json"), "utf8"), "{broken");
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("bounds receipts without evicting unexpired actions", async () => {
  const store = createActionReceiptStore(undefined, { maxRecords: 1 });
  await store.initialize();
  await store.reserveMessage({
    sessionId: "session-1",
    clientMessageId: "44444444-4444-4444-8444-444444444444",
    payloadHash: "a".repeat(64),
    delivery: { disposition: "started" },
    state: "accepted"
  });

  await assert.rejects(
    store.reserveMessage({
      sessionId: "session-1",
      clientMessageId: "55555555-5555-4555-8555-555555555555",
      payloadHash: "b".repeat(64),
      delivery: { disposition: "started" },
      state: "accepted"
    }),
    (error) => error.message === "idempotency_capacity_exceeded" && error.status === 503
  );
});

test("removes expired receipts during restart cleanup", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-action-receipts-"));
  let currentTime = new Date("2026-07-17T00:00:00.000Z");
  try {
    const firstStore = createActionReceiptStore(directory, {
      now: () => currentTime,
      lifetimeMs: 1000,
      maxRecords: 1
    });
    await firstStore.initialize();
    await firstStore.reserveMessage({
      sessionId: "session-1",
      clientMessageId: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
      payloadHash: "a".repeat(64),
      delivery: { disposition: "started" },
      state: "accepted"
    });

    currentTime = new Date("2026-07-17T00:00:02.000Z");
    const restarted = createActionReceiptStore(directory, {
      now: () => currentTime,
      lifetimeMs: 1000,
      maxRecords: 1
    });
    await restarted.initialize();
    await restarted.reserveMessage({
      sessionId: "session-1",
      clientMessageId: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
      payloadHash: "b".repeat(64),
      delivery: { disposition: "started" },
      state: "accepted"
    });

    const saved = JSON.parse(await readFile(join(directory, "action-receipts.json"), "utf8"));
    assert.equal(saved.records.length, 1);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("persists approval notification and decision receipts without raw approval identities", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-action-receipts-"));
  try {
    const firstStore = createActionReceiptStore(directory);
    await firstStore.initialize();
    const notification = await firstStore.reserveApprovalNotification({
      sessionId: "approval-session-private",
      accountId: "approval-account-private",
      threadId: "approval-thread-private",
      turnId: "approval-turn-private",
      itemId: "approval-item-private",
      runtimeRequestId: 91,
      approvalId: "approval-public-id"
    });
    await firstStore.reserveApprovalDecision({
      sessionId: "approval-session-private",
      approvalId: "approval-public-id",
      decision: "approved",
      delivery: { approvalId: "approval-public-id", status: "approved" },
      state: "resolved"
    });

    const restarted = createActionReceiptStore(directory);
    await restarted.initialize();
    const duplicate = await restarted.reserveApprovalNotification({
      sessionId: "approval-session-private",
      accountId: "approval-account-private",
      threadId: "approval-thread-private",
      turnId: "approval-turn-private",
      itemId: "approval-item-private",
      runtimeRequestId: 91,
      approvalId: "must-not-replace"
    });
    const decision = await restarted.findApprovalDecision({
      sessionId: "approval-session-private",
      approvalId: "approval-public-id",
      decision: "approved"
    });
    const raw = await readFile(join(directory, "action-receipts.json"), "utf8");

    assert.equal(notification.created, true);
    assert.equal(duplicate.created, false);
    assert.equal(duplicate.receipt.approvalId, "approval-public-id");
    assert.deepEqual(decision.delivery, { approvalId: "approval-public-id", status: "approved" });
    for (const privateIdentity of [
      "approval-session-private",
      "approval-account-private",
      "approval-thread-private",
      "approval-turn-private",
      "approval-item-private"
    ]) {
      assert.equal(raw.includes(privateIdentity), false);
    }
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("rejects a conflicting decision for the same approval identity", async () => {
  const store = createActionReceiptStore();
  await store.initialize();
  await store.reserveApprovalDecision({
    sessionId: "session-approval-conflict",
    approvalId: "approval-conflict",
    decision: "approved",
    delivery: { approvalId: "approval-conflict", status: "approved" },
    state: "resolved"
  });

  await assert.rejects(
    store.findApprovalDecision({
      sessionId: "session-approval-conflict",
      approvalId: "approval-conflict",
      decision: "rejected"
    }),
    (error) => error.message === "idempotency_conflict" && error.status === 409
  );
});

test("persists a queued action receipt and rejects a conflicting action", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-action-receipts-"));
  try {
    const firstStore = createActionReceiptStore(directory);
    await firstStore.initialize();
    await firstStore.reserveQueuedAction({
      sessionId: "queue-session-private",
      queuedMessageId: "queue-message-private",
      action: "steer",
      delivery: { queuedMessageId: "queue-message-private", action: "steer" },
      state: "completed"
    });

    const restarted = createActionReceiptStore(directory);
    await restarted.initialize();
    const replay = await restarted.findQueuedAction({
      sessionId: "queue-session-private",
      queuedMessageId: "queue-message-private",
      action: "steer"
    });
    const raw = await readFile(join(directory, "action-receipts.json"), "utf8");

    assert.deepEqual(replay.delivery, { queuedMessageId: "queue-message-private", action: "steer" });
    assert.equal(raw.includes("queue-session-private"), false);
    await assert.rejects(
      restarted.findQueuedAction({
        sessionId: "queue-session-private",
        queuedMessageId: "queue-message-private",
        action: "delete"
      }),
      (error) => error.message === "idempotency_conflict" && error.status === 409
    );
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});
