// Unit tests for formatAuditSummary and its helpers.
//
// These run with Node 24's built-in test runner + native TypeScript support,
// importing the .ts module directly. We mock the i18next `t` function with a
// simple recorder so assertions can target both:
//   1) which translation key was selected (rename vs. update vs. id-only,
//      EN vs. AR), and
//   2) which interpolation values were forwarded into that key.
import { test } from "node:test";
import assert from "node:assert/strict";
import { formatAuditSummary } from "../lib/audit-summary.ts";

// `t` mock that returns a deterministic, easy-to-assert string of the form
// `key{json}`. Tests parse out the key and the interpolation payload.
function makeT() {
  return (key, options) => {
    const opts = options ?? {};
    return `${key}${JSON.stringify(opts)}`;
  };
}

function entry(overrides) {
  return {
    id: 1,
    action: "noop",
    targetType: "app",
    targetId: 100,
    metadata: {},
    createdAt: "2026-04-30T00:00:00.000Z",
    actor: null,
    ...overrides,
  };
}

function parse(result) {
  if (result == null) return { key: null, opts: null };
  const idx = result.indexOf("{");
  if (idx === -1) return { key: result, opts: {} };
  return {
    key: result.slice(0, idx),
    opts: JSON.parse(result.slice(idx)),
  };
}

test("app.update emits rename branch when nameEn changes (EN locale)", () => {
  const result = formatAuditSummary(
    entry({
      action: "app.update",
      metadata: {
        nameEn: "Tx New",
        nameAr: "تي إكس الجديد",
        slug: "tx",
        changes: {
          nameEn: { from: "Tx Old", to: "Tx New" },
        },
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.app.rename");
  assert.equal(opts.previousName, "Tx Old");
  assert.equal(opts.name, "Tx New");
});

test("app.update prefers Arabic rename diff when language is AR", () => {
  // Both EN and AR were edited in the same patch. The AR-locale formatter
  // should announce the AR rename, not the EN one.
  const result = formatAuditSummary(
    entry({
      action: "app.update",
      metadata: {
        nameEn: "Tx New",
        nameAr: "تي إكس الجديد",
        changes: {
          nameEn: { from: "Tx Old", to: "Tx New" },
          nameAr: { from: "تي إكس القديم", to: "تي إكس الجديد" },
        },
      },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.app.rename");
  assert.equal(opts.previousName, "تي إكس القديم");
  assert.equal(opts.name, "تي إكس الجديد");
});

test("app.update without rename falls back to generic update summary", () => {
  const result = formatAuditSummary(
    entry({
      action: "app.update",
      metadata: {
        nameEn: "Tx",
        nameAr: "تي إكس",
        changes: {
          color: { from: "blue", to: "red" },
          iconName: { from: "a", to: "b" },
        },
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.app.update");
  assert.equal(opts.name, "Tx");
  // changes count goes through unitLabel → "admin.audit.unit.change{count:2}"
  assert.equal(opts.changes, `admin.audit.unit.change${JSON.stringify({ count: 2 })}`);
});

test("group.update emits rename branch when previousName differs", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.update",
      targetType: "group",
      targetId: 7,
      metadata: {
        name: "Eng",
        previousName: "Engineering",
        changes: { name: { from: "Engineering", to: "Eng" } },
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.rename");
  assert.equal(opts.previousName, "Engineering");
  assert.equal(opts.name, "Eng");
});

test("group.update with multiple field changes uses generic update branch", () => {
  // Multi-field updates are no longer rename-only, so the rename copy is
  // skipped and we fall through to the generic '... · N changes' summary.
  const result = formatAuditSummary(
    entry({
      action: "group.update",
      targetType: "group",
      targetId: 7,
      metadata: {
        name: "Eng",
        previousName: "Engineering",
        changes: {
          name: { from: "Engineering", to: "Eng" },
          descriptionEn: { from: "old", to: "new" },
        },
      },
    }),
    makeT(),
    "en",
  );
  const { key } = parse(result);
  assert.equal(key, "admin.audit.summary.group.update");
});

test("settings.update for registrationOpen=true alone uses opened summary", () => {
  const result = formatAuditSummary(
    entry({
      action: "settings.update",
      targetType: "settings",
      targetId: null,
      metadata: { changes: { registrationOpen: { from: false, to: true } } },
    }),
    makeT(),
    "en",
  );
  const { key } = parse(result);
  assert.equal(key, "admin.audit.summary.settings.registrationOpened");
});

test("settings.update for registrationOpen=false alone uses closed summary", () => {
  const result = formatAuditSummary(
    entry({
      action: "settings.update",
      targetType: "settings",
      targetId: null,
      metadata: { changes: { registrationOpen: { from: true, to: false } } },
    }),
    makeT(),
    "en",
  );
  const { key } = parse(result);
  assert.equal(key, "admin.audit.summary.settings.registrationClosed");
});

test("settings.update with registration toggle plus other fields uses '...With' variant", () => {
  const result = formatAuditSummary(
    entry({
      action: "settings.update",
      targetType: "settings",
      targetId: null,
      metadata: {
        changes: {
          registrationOpen: { from: false, to: true },
          siteTitleEn: { from: "old", to: "new" },
        },
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.settings.registrationOpenedWith");
  assert.equal(opts.others, `admin.audit.unit.change${JSON.stringify({ count: 1 })}`);
});

test("group.user.add prefers username when present", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.user.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", userId: 42, username: "alice" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.userAdd");
  assert.equal(opts.username, "alice");
  assert.equal(opts.name, "Engineering");
});

test("group.user.add falls back to id-only branch when username missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.user.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", userId: 42 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.userAddById");
  assert.equal(opts.userId, 42);
});

test("group.user.remove prefers username when present", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.user.remove",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", userId: 42, username: "alice" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.userRemove");
  assert.equal(opts.username, "alice");
});

test("group.user.remove falls back to id-only branch when username missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.user.remove",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", userId: 42 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.userRemoveById");
  assert.equal(opts.userId, 42);
});

test("group.app.add uses appNameEn for English locale", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.app.add",
      targetType: "group",
      targetId: 5,
      metadata: {
        groupName: "Engineering",
        appId: 12,
        appSlug: "tx",
        appNameEn: "Tx App",
        appNameAr: "تطبيق",
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.appAdd");
  assert.equal(opts.appName, "Tx App");
});

test("group.app.add uses appNameAr for Arabic locale", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.app.add",
      targetType: "group",
      targetId: 5,
      metadata: {
        groupName: "Engineering",
        appId: 12,
        appSlug: "tx",
        appNameEn: "Tx App",
        appNameAr: "تطبيق",
      },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.appAdd");
  assert.equal(opts.appName, "تطبيق");
});

test("group.app.add falls back to slug when both localized names are missing", () => {
  // appSlug satisfies linkedAppName(), so this still hits the named branch.
  const result = formatAuditSummary(
    entry({
      action: "group.app.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", appId: 12, appSlug: "tx" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.appAdd");
  assert.equal(opts.appName, "tx");
});

test("group.app.add falls back to id-only branch when no name and no slug", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.app.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", appId: 12 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.appAddById");
  assert.equal(opts.appId, 12);
});

test("group.app.remove falls back to id-only branch when name and slug are missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.app.remove",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", appId: 12 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.appRemoveById");
  assert.equal(opts.appId, 12);
});

test("group.role.add prefers roleName when present", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.role.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", roleId: 9, roleName: "manager" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.roleAdd");
  assert.equal(opts.roleName, "manager");
});

test("group.role.add falls back to id-only branch when roleName missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.role.add",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", roleId: 9 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.roleAddById");
  assert.equal(opts.roleId, 9);
});

test("group.role.remove prefers roleName when present", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.role.remove",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", roleId: 9, roleName: "manager" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.roleRemove");
  assert.equal(opts.roleName, "manager");
});

test("group.role.remove falls back to id-only branch when roleName missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "group.role.remove",
      targetType: "group",
      targetId: 5,
      metadata: { groupName: "Engineering", roleId: 9 },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.group.roleRemoveById");
  assert.equal(opts.roleId, 9);
});

test("unknown action returns null so callers can hide the summary line", () => {
  const result = formatAuditSummary(
    entry({ action: "totally.unknown.event" }),
    makeT(),
    "en",
  );
  assert.equal(result, null);
});

// #178: cover the user.delete branches added by #194 (display name resolution
// across EN/AR locales) and the user.force_delete branches that share the same
// shape, so the formatter doesn't silently regress when callers tweak metadata.

test("user.delete uses id-only key when username is missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 42,
      metadata: {},
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.delete");
  assert.equal(opts.username, "#42");
});

test("user.delete uses delete (no name) key when only username present", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: { username: "alice" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.delete");
  assert.equal(opts.username, "alice");
});

test("user.delete picks displayNameEn in EN locale when both names present", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: {
        username: "alice",
        displayNameEn: "Alice Smith",
        displayNameAr: "أليس",
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.deleteWithName");
  assert.equal(opts.username, "alice");
  assert.equal(opts.displayName, "Alice Smith");
});

test("user.delete picks displayNameAr in AR locale when both names present", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: {
        username: "alice",
        displayNameEn: "Alice Smith",
        displayNameAr: "أليس",
      },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.deleteWithName");
  assert.equal(opts.displayName, "أليس");
});

test("user.delete falls back to displayNameEn in AR locale when AR missing", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: { username: "alice", displayNameEn: "Alice Smith" },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.deleteWithName");
  assert.equal(opts.displayName, "Alice Smith");
});

test("user.delete with force flag in metadata routes to forceDelete branch", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: { username: "bob", force: true },
    }),
    makeT(),
    "en",
  );
  const { key } = parse(result);
  assert.equal(key, "admin.audit.summary.user.forceDelete");
});

test("user.delete with force + display name uses forceDeleteWithName branch", () => {
  const result = formatAuditSummary(
    entry({
      action: "user.delete",
      targetType: "user",
      targetId: 7,
      metadata: {
        username: "bob",
        displayNameEn: "Bob Jones",
        force: true,
      },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.user.forceDeleteWithName");
  assert.equal(opts.displayName, "Bob Jones");
});

// #195: plain service.delete now writes an audit row; cover the new summary
// branch (with/without nameEn/nameAr) plus the existing service.force_delete
// path which now also reads nameAr in the AR locale.

test("service.delete uses deleteId fallback when no name in metadata", () => {
  const result = formatAuditSummary(
    entry({
      action: "service.delete",
      targetType: "service",
      targetId: 99,
      metadata: {},
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.service.deleteId");
  assert.equal(opts.id, 99);
});

test("service.delete prefers nameEn in EN locale", () => {
  const result = formatAuditSummary(
    entry({
      action: "service.delete",
      targetType: "service",
      targetId: 99,
      metadata: { nameEn: "Coffee", nameAr: "قهوة" },
    }),
    makeT(),
    "en",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.service.delete");
  assert.equal(opts.name, "Coffee");
});

test("service.delete prefers nameAr in AR locale", () => {
  const result = formatAuditSummary(
    entry({
      action: "service.delete",
      targetType: "service",
      targetId: 99,
      metadata: { nameEn: "Coffee", nameAr: "قهوة" },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.service.delete");
  assert.equal(opts.name, "قهوة");
});

test("service.force_delete uses nameAr in AR locale when both names present", () => {
  const result = formatAuditSummary(
    entry({
      action: "service.force_delete",
      targetType: "service",
      targetId: 99,
      metadata: { nameEn: "Coffee", nameAr: "قهوة", orderCount: 3 },
    }),
    makeT(),
    "ar",
  );
  const { key, opts } = parse(result);
  assert.equal(key, "admin.audit.summary.service.forceDelete");
  assert.equal(opts.name, "قهوة");
});
