import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  EntityTypeNotRegisteredError,
  InvalidActorError,
  InvalidOperationTypeError,
  ScopeMismatchError,
  MissingEntityIdError,
  MissingEventIdError,
  ValidationErrorError,
} from "../lib/errors.generated";
import {
  companyBoundEntity,
  globalEntity,
  activePolicy,
  captureFieldRule,
  maskFieldRule,
  hashFieldRule,
  excludeFieldRule,
  baseAuditEntry,
} from "../testing/fixtures";
import { run, type LogAuditEventInput } from "./logAuditEvent";
import { logAuditEvent } from "./logAuditEvent.generated";
import { type CommandContext, InsufficientPermissionError } from "@tailor-platform/erp-kit/module";

const ctx: CommandContext = {
  actorId: "test-actor",
  permissions: ["audit:logAuditEvent"],
};

function baseInput(overrides?: Partial<LogAuditEventInput>): LogAuditEventInput {
  return {
    eventId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    actorType: "USER",
    actorId: "user-1",
    entityType: "SalesOrder",
    entityId: "so-1",
    operationType: "UPDATE",
    companyId: "company-1",
    changes: [{ fieldName: "amount", oldValue: "100.00", newValue: "95.00" }],
    ...overrides,
  };
}

function setupHappyPath(spies: ReturnType<typeof createMockDb<Transaction>>["spies"]) {
  spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
  spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup
  spies.select.mockReturnValueOnce(activePolicy); // active policy lookup
  spies.select.mockReturnValueOnce([captureFieldRule]); // field rules (execute returns array)
  spies.insert.mockReturnValue(baseAuditEntry); // audit entry insert
}

describe("logAuditEvent", () => {
  it("creates AuditEntry with all required fields for a CREATE operation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup
    spies.select.mockReturnValueOnce(createPolicy); // policy
    spies.select.mockReturnValueOnce([captureFieldRule]); // field rules
    const createEntry = { ...baseAuditEntry, operationType: "CREATE" };
    spies.insert.mockReturnValue(createEntry);

    const result = await run(
      db,
      baseInput({
        operationType: "CREATE",
        changes: [{ fieldName: "amount", newValue: "100.00" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(true);
      expect(result.value.auditEntry).toBeDefined();
    }
  });

  it("creates AuditEntry with all required fields for an UPDATE operation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(db, baseInput(), ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(true);
      expect(result.value.auditEntry).toBeDefined();
    }
  });

  it("creates AuditEntry with all required fields for a DELETE operation", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(deletePolicy);
    spies.select.mockReturnValueOnce([captureFieldRule]);
    const deleteEntry = { ...baseAuditEntry, operationType: "DELETE" };
    spies.insert.mockReturnValue(deleteEntry);

    const result = await run(
      db,
      baseInput({
        operationType: "DELETE",
        changes: [{ fieldName: "amount", oldValue: "100.00" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(true);
    }
  });

  it("silently skips AuditEntry creation for UPDATE with empty changes array (zero-delta no-op)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup
    spies.select.mockReturnValueOnce(activePolicy); // policy

    const result = await run(db, baseInput({ changes: [] }), ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("rejects CREATE with empty changes array", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup
    spies.select.mockReturnValueOnce(createPolicy); // policy

    const result = await run(db, baseInput({ operationType: "CREATE", changes: [] }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ValidationErrorError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("rejects DELETE with empty changes array", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup
    spies.select.mockReturnValueOnce(deletePolicy); // policy

    const result = await run(db, baseInput({ operationType: "DELETE", changes: [] }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ValidationErrorError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("silently discards duplicate eventId without creating a new entry", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseAuditEntry); // duplicate found

    const result = await run(db, baseInput(), ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
      expect(result.value.auditEntry).toEqual(baseAuditEntry);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("rejects event with unregistered entityType", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(undefined); // entity not found

    const result = await run(db, baseInput({ entityType: "UnknownEntity" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(EntityTypeNotRegisteredError);
    }
  });

  it("rejects event with missing actorType", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity found

    const result = await run(db, baseInput({ actorType: "" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidActorError);
    }
  });

  it("rejects event with invalid actorType", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);

    const result = await run(db, baseInput({ actorType: "ROBOT" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidActorError);
    }
  });

  it("rejects event with empty actorId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);

    const result = await run(db, baseInput({ actorId: "" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidActorError);
    }
  });

  it("rejects event with invalid operationType", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);

    const result = await run(db, baseInput({ operationType: "ARCHIVE" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidOperationTypeError);
    }
  });

  it("rejects event with companyId on a global entity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(globalEntity);

    const result = await run(db, baseInput({ entityType: "User", companyId: "company-1" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ScopeMismatchError);
    }
  });

  it("rejects event with null companyId on a company-bound entity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);

    const result = await run(db, baseInput({ companyId: undefined }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ScopeMismatchError);
    }
  });

  it("silently discards event when no active policy matches entity + operation + scope", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity found
    spies.select.mockReturnValueOnce(undefined); // no active policy

    const result = await run(db, baseInput(), ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.auditEntry).toBeNull();
      expect(result.value.created).toBe(false);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("generates system timestamp and ignores caller-supplied timestamp", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(db, baseInput(), ctx);

    expect(result.ok).toBe(true);
    // The values spy captures what was passed to insert; the timestamp should be a Date
    expect(spies.values).toHaveBeenCalled();
    const insertedValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(insertedValues.timestamp).toBeInstanceOf(Date);
  });

  it("creates ChangeDetail records linked to the parent AuditEntry", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(db, baseInput(), ctx);

    expect(result.ok).toBe(true);
    // First insert call is AuditEntry, second is ChangeDetail
    expect(spies.values).toHaveBeenCalledTimes(2);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.auditEntryId).toBe(baseAuditEntry.id);
    expect(changeValues.fieldName).toBe("amount");
  });

  it("applies MASK sensitivity mode to field values", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(activePolicy); // policy
    spies.select.mockReturnValueOnce([maskFieldRule]); // mask rule for customerId
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [{ fieldName: "customerId", oldValue: "CUST-12345", newValue: "CUST-67890" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    // Verify masked values in change detail insert
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe("******2345");
    expect(changeValues.newValue).toBe("******7890");
  });

  it("applies HASH sensitivity mode to field values", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(activePolicy);
    spies.select.mockReturnValueOnce([hashFieldRule]); // hash rule for status
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({ changes: [{ fieldName: "status", oldValue: "OPEN", newValue: "CLOSED" }] }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    // Hash values should be SHA-256 hex strings (64 chars), not the original
    expect(changeValues.oldValue).not.toBe("OPEN");
    expect(changeValues.newValue).not.toBe("CLOSED");
    expect(typeof changeValues.oldValue).toBe("string");
    expect(typeof changeValues.newValue).toBe("string");
    expect(changeValues.oldValue).toHaveLength(64);
    expect(changeValues.newValue).toHaveLength(64);
    expect(changeValues.oldValue).toMatch(/^[0-9a-f]{64}$/);
    expect(changeValues.newValue).toMatch(/^[0-9a-f]{64}$/);
  });

  it("applies EXCLUDE sensitivity mode storing null for both oldValue and newValue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(activePolicy);
    spies.select.mockReturnValueOnce([excludeFieldRule]); // exclude rule for amount
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({ changes: [{ fieldName: "amount", oldValue: "100.00", newValue: "95.00" }] }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBeNull();
  });

  it("serializes scalar field values as strings", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(
      db,
      baseInput({ changes: [{ fieldName: "amount", oldValue: "100", newValue: "200" }] }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(typeof changeValues.oldValue).toBe("string");
    expect(typeof changeValues.newValue).toBe("string");
  });

  it("strips rich text / HTML values to plain text before storage", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const richtextEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "amount", fieldType: "richtext" }]),
    };
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(richtextEntity); // entity lookup
    spies.select.mockReturnValueOnce(activePolicy); // active policy lookup
    spies.select.mockReturnValueOnce([captureFieldRule]); // field rules
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [
          {
            fieldName: "amount",
            oldValue: "<b>100</b>",
            newValue: "<p>200</p>",
            fieldType: "richtext" as const,
          },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe("100");
    expect(changeValues.newValue).toBe("200");
  });

  it("serializes collection / array field values as JSON strings", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const arrayEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "tags", fieldType: "array" }]),
    };
    const arrayFieldRule = { ...captureFieldRule, fieldName: "tags" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(arrayEntity); // entity lookup with array field type
    spies.select.mockReturnValueOnce(activePolicy); // active policy lookup
    spies.select.mockReturnValueOnce([arrayFieldRule]); // field rules
    spies.insert.mockReturnValue(baseAuditEntry);

    // Array values passed as actual arrays — normalizeFieldValue should JSON.stringify them
    const result = await run(
      db,
      baseInput({
        changes: [{ fieldName: "tags", oldValue: ["a", "b"], newValue: ["a", "b", "c"] }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe(JSON.stringify(["a", "b"]));
    expect(changeValues.newValue).toBe(JSON.stringify(["a", "b", "c"]));
  });

  it("captures relation field values as foreign key ID strings", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // customerId is registered as "relation" in fixtures
    const relationPolicy = { ...activePolicy };
    const relationFieldRule = { ...captureFieldRule, fieldName: "customerId" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity lookup (customerId is relation)
    spies.select.mockReturnValueOnce(relationPolicy); // active policy lookup
    spies.select.mockReturnValueOnce([relationFieldRule]); // field rules
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [
          {
            fieldName: "customerId",
            oldValue: { id: "fk-id-123", name: "Old Relation" },
            newValue: { id: "fk-id-456", name: "New Relation" },
            fieldType: "relation" as const,
          },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe("fk-id-123");
    expect(changeValues.newValue).toBe("fk-id-456");
  });

  it("captures file attachment fields as reference metadata only (file ID, filename, size)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const fileEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "attachment", fieldType: "file" }]),
    };
    const fileFieldRule = { ...captureFieldRule, fieldName: "attachment" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(fileEntity); // entity lookup with file field type
    spies.select.mockReturnValueOnce(activePolicy); // active policy lookup
    spies.select.mockReturnValueOnce([fileFieldRule]); // field rules
    spies.insert.mockReturnValue(baseAuditEntry);

    // Pass file metadata as an object — normalizeFieldValue should serialize it
    const fileMeta = { fileId: "f-1", filename: "doc.pdf", size: 1024 };
    const result = await run(
      db,
      baseInput({
        changes: [{ fieldName: "attachment", oldValue: null, newValue: fileMeta }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.newValue).toBe(
      JSON.stringify({ fileId: "f-1", filename: "doc.pdf", size: 1024 }),
    );
  });

  it("excludes binary / BLOB fields from audit capture", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // Register entity with a binary field so the exclusion happens via effectiveFieldType filtering
    const binaryEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([
        { fieldName: "amount", fieldType: "scalar" },
        { fieldName: "binaryData", fieldType: "binary" },
      ]),
    };
    const binaryFieldRules = [
      captureFieldRule,
      { ...captureFieldRule, id: "rule-bin", fieldName: "binaryData" },
    ];
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(binaryEntity); // entity with binary field
    spies.select.mockReturnValueOnce(activePolicy); // active policy
    spies.select.mockReturnValueOnce(binaryFieldRules); // field rules include binary field
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [
          { fieldName: "amount", oldValue: "100", newValue: "200" },
          { fieldName: "binaryData", oldValue: "AQID", newValue: "BAUG" },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    // Only 2 insert calls: AuditEntry + 1 ChangeDetail (binaryData dropped by fieldType filter)
    expect(spies.values).toHaveBeenCalledTimes(2);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.fieldName).toBe("amount");
  });

  it("excludes computed / derived fields from audit capture", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // Register entity with a computed field so the exclusion happens via effectiveFieldType filtering
    const computedEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([
        { fieldName: "amount", fieldType: "scalar" },
        { fieldName: "computedTotal", fieldType: "computed" },
      ]),
    };
    const computedFieldRules = [
      captureFieldRule,
      { ...captureFieldRule, id: "rule-comp", fieldName: "computedTotal" },
    ];
    spies.select.mockReturnValueOnce(undefined); // no duplicate eventId
    spies.select.mockReturnValueOnce(computedEntity); // entity with computed field
    spies.select.mockReturnValueOnce(activePolicy); // active policy
    spies.select.mockReturnValueOnce(computedFieldRules); // field rules include computed field
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [
          { fieldName: "amount", oldValue: "100", newValue: "200" },
          { fieldName: "computedTotal", oldValue: "500", newValue: "600" },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    // Only 2 insert calls: AuditEntry + 1 ChangeDetail (computedTotal dropped by fieldType filter)
    expect(spies.values).toHaveBeenCalledTimes(2);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.fieldName).toBe("amount");
  });

  it("silently drops fields from changes array that are not in entity's registered auditableFields", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(
      db,
      baseInput({
        changes: [
          { fieldName: "amount", oldValue: "100", newValue: "200" },
          { fieldName: "unknownField", oldValue: "a", newValue: "b" },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    // AuditEntry insert + 1 ChangeDetail (unknownField dropped)
    expect(spies.values).toHaveBeenCalledTimes(2);
  });

  it("silently drops fields from changes array that are not in the active policy's field rules when policy has explicit rules", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(activePolicy); // policy
    // Only captureFieldRule for "amount" — "status" has no rule
    spies.select.mockReturnValueOnce([captureFieldRule]);
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [
          { fieldName: "amount", oldValue: "100", newValue: "200" },
          { fieldName: "status", oldValue: "OPEN", newValue: "CLOSED" },
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    // AuditEntry insert + 1 ChangeDetail (status dropped by policy rules)
    expect(spies.values).toHaveBeenCalledTimes(2);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.fieldName).toBe("amount");
  });

  it("truncates values exceeding 4,000 characters with [truncated] suffix", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const longValue = "x".repeat(5000);
    const result = await run(
      db,
      baseInput({ changes: [{ fieldName: "amount", oldValue: "100", newValue: longValue }] }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.newValue).toHaveLength(4000);
    expect(changeValues.newValue).toMatch(/\[truncated\]$/);
  });

  it("stores correlationId when provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const correlationId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
    const result = await run(db, baseInput({ correlationId }), ctx);

    expect(result.ok).toBe(true);
    const entryValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(entryValues.correlationId).toBe(correlationId);
  });

  it("stores actorMetadata (ipAddress, userAgent, sessionId, requestId) when provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(
      db,
      baseInput({
        ipAddress: "192.168.1.1",
        userAgent: "TestAgent/1.0",
        sessionId: "sess-1",
        requestId: "req-1",
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const entryValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(entryValues.ipAddress).toBe("192.168.1.1");
    expect(entryValues.userAgent).toBe("TestAgent/1.0");
    expect(entryValues.sessionId).toBe("sess-1");
    expect(entryValues.requestId).toBe("req-1");
  });

  it("stores onBehalfOf when provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(db, baseInput({ onBehalfOf: "user-3" }), ctx);

    expect(result.ok).toBe(true);
    const entryValues = spies.values.mock.calls[0][0] as Record<string, unknown>;
    expect(entryValues.onBehalfOf).toBe("user-3");
  });

  it("creates ChangeDetail with null oldValue for CREATE operations", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(createPolicy);
    spies.select.mockReturnValueOnce([captureFieldRule]);
    spies.insert.mockReturnValue({ ...baseAuditEntry, operationType: "CREATE" });

    const result = await run(
      db,
      baseInput({
        operationType: "CREATE",
        changes: [{ fieldName: "amount", oldValue: null, newValue: "100.00" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBe("100.00");
  });

  it("creates ChangeDetail with null newValue for DELETE operations", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(deletePolicy);
    spies.select.mockReturnValueOnce([captureFieldRule]);
    spies.insert.mockReturnValue({ ...baseAuditEntry, operationType: "DELETE" });

    const result = await run(
      db,
      baseInput({
        operationType: "DELETE",
        changes: [{ fieldName: "amount", oldValue: "100.00", newValue: null }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe("100.00");
    expect(changeValues.newValue).toBeNull();
  });

  it("filters out unchanged fields for UPDATE operations", async () => {
    const { db, spies } = createMockDb<Transaction>();
    setupHappyPath(spies);

    const result = await run(
      db,
      baseInput({
        changes: [
          { fieldName: "amount", oldValue: "100.00", newValue: "200.00" },
          { fieldName: "status", oldValue: "OPEN", newValue: "OPEN" }, // unchanged
        ],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(true);
    }
    // AuditEntry + 1 ChangeDetail (unchanged "status" dropped)
    expect(spies.values).toHaveBeenCalledTimes(2);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.fieldName).toBe("amount");
  });

  it("returns no-op when all UPDATE changes are unchanged after normalization", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(activePolicy); // policy
    spies.select.mockReturnValueOnce([captureFieldRule]); // field rules
    spies.insert.mockReturnValue(baseAuditEntry);

    const result = await run(
      db,
      baseInput({
        changes: [{ fieldName: "amount", oldValue: "100.00", newValue: "100.00" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
      expect(result.value.auditEntry).toBeNull();
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("enforces oldValue=null for CREATE even if caller provides oldValue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(createPolicy);
    spies.select.mockReturnValueOnce([captureFieldRule]);
    spies.insert.mockReturnValue({ ...baseAuditEntry, operationType: "CREATE" });

    const result = await run(
      db,
      baseInput({
        operationType: "CREATE",
        changes: [{ fieldName: "amount", oldValue: "should-be-null", newValue: "100.00" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBe("100.00");
  });

  it("enforces newValue=null for DELETE even if caller provides newValue", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);
    spies.select.mockReturnValueOnce(deletePolicy);
    spies.select.mockReturnValueOnce([captureFieldRule]);
    spies.insert.mockReturnValue({ ...baseAuditEntry, operationType: "DELETE" });

    const result = await run(
      db,
      baseInput({
        operationType: "DELETE",
        changes: [{ fieldName: "amount", oldValue: "100.00", newValue: "should-be-null" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    const changeValues = spies.values.mock.calls[1][0] as Record<string, unknown>;
    expect(changeValues.oldValue).toBe("100.00");
    expect(changeValues.newValue).toBeNull();
  });

  it("rejects event with empty entityId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    spies.select.mockReturnValueOnce(companyBoundEntity);

    const result = await run(db, baseInput({ entityId: "" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingEntityIdError);
    }
  });

  it("rejects event with non-UUID correlationId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity found

    const result = await run(db, baseInput({ correlationId: "not-a-valid-uuid" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ValidationErrorError);
    }
  });

  it("rejects event with empty eventId", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, baseInput({ eventId: "" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingEventIdError);
    }
  });

  it("rejects event with non-UUID eventId", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(db, baseInput({ eventId: "not-a-uuid" }), ctx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(MissingEventIdError);
    }
  });

  it("rejects CREATE when all changes are filtered out by policy rules", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(createPolicy); // policy
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    spies.select.mockReturnValueOnce([captureFieldRule]);

    const result = await run(
      db,
      baseInput({
        operationType: "CREATE",
        changes: [{ fieldName: "status", newValue: "OPEN" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ValidationErrorError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("rejects DELETE when all changes are filtered out by policy rules", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(deletePolicy); // policy
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    spies.select.mockReturnValueOnce([captureFieldRule]);

    const result = await run(
      db,
      baseInput({
        operationType: "DELETE",
        changes: [{ fieldName: "status", oldValue: "OPEN" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ValidationErrorError);
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("returns no-op for UPDATE when all changes are filtered out by policy rules", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined); // no duplicate
    spies.select.mockReturnValueOnce(companyBoundEntity); // entity
    spies.select.mockReturnValueOnce(activePolicy); // policy
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    spies.select.mockReturnValueOnce([captureFieldRule]);

    const result = await run(
      db,
      baseInput({
        changes: [{ fieldName: "status", oldValue: "OPEN", newValue: "CLOSED" }],
      }),
      ctx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
      expect(result.value.auditEntry).toBeNull();
    }
    expect(spies.insert).not.toHaveBeenCalled();
  });

  it("rejects caller without logAuditEvent permission", async () => {
    const { db } = createMockDb<Transaction>();
    const command = logAuditEvent();
    const noPermCtx: CommandContext = {
      actorId: "test-actor",
      permissions: [],
    };

    const result = await command(db, baseInput(), noPermCtx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InsufficientPermissionError);
    }
  });
});
