import { type CommandContext, InsufficientPermissionError } from "@tailor-platform/erp-kit/core";
import { createKyselyMock, type KyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } 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";

const ctx: CommandContext = {
  actorId: "test-actor",
  permissions: ["audit: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(mock: KyselyMock<Namespace["main-db"]>) {
  mock.enqueueResults(
    [], // no duplicate eventId
    [companyBoundEntity], // entity lookup
    [activePolicy], // active policy lookup
    [captureFieldRule], // field rules (execute returns array)
    [baseAuditEntry], // audit entry insert
  );
}

describe("logAuditEvent", () => {
  it("creates AuditEntry with all required fields for a CREATE operation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    const createEntry = { ...baseAuditEntry, operationType: "CREATE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [createPolicy],
      [captureFieldRule],
      [createEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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 mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    const deleteEntry = { ...baseAuditEntry, operationType: "DELETE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [deletePolicy],
      [captureFieldRule],
      [deleteEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity], [activePolicy]);

    const result = await mock.withTx((trx) => run(trx, baseInput({ changes: [] }), ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("rejects CREATE with empty changes array", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    mock.enqueueResults([], [companyBoundEntity], [createPolicy]);

    const result = await mock.withTx((trx) =>
      run(trx, baseInput({ operationType: "CREATE", changes: [] }), ctx),
    );

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

  it("rejects DELETE with empty changes array", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    mock.enqueueResults([], [companyBoundEntity], [deletePolicy]);

    const result = await mock.withTx((trx) =>
      run(trx, baseInput({ operationType: "DELETE", changes: [] }), ctx),
    );

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

  it("silently discards duplicate eventId without creating a new entry", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAuditEntry]); // duplicate found

    const result = await mock.withTx((trx) => run(trx, baseInput(), ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.created).toBe(false);
      expect(result.value.auditEntry).toEqual(baseAuditEntry);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("rejects event with unregistered entityType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], []); // no duplicate, entity not found

    const result = await mock.withTx((trx) =>
      run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]); // no duplicate, entity found

    const result = await mock.withTx((trx) => run(trx, baseInput({ actorType: "" }), ctx));

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

  it("rejects event with invalid actorType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]);

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]);

    const result = await mock.withTx((trx) => run(trx, baseInput({ actorId: "" }), ctx));

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

  it("rejects event with invalid operationType", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]);

    const result = await mock.withTx((trx) =>
      run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [globalEntity]);

    const result = await mock.withTx((trx) =>
      run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]);

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity], []); // no duplicate, entity found, no active policy

    const result = await mock.withTx((trx) => run(trx, baseInput(), ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.auditEntry).toBeNull();
      expect(result.value.created).toBe(false);
    }
    expect(mock.inserts).toHaveLength(0);
  });

  it("generates system timestamp and ignores caller-supplied timestamp", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) => run(trx, baseInput(), ctx));

    expect(result.ok).toBe(true);
    // The first insert is AuditEntry; the timestamp should be a Date
    expect(mock.inserts.length).toBeGreaterThan(0);
    const insertedValues = mock.inserts[0].insertValues();
    expect(insertedValues.timestamp).toBeInstanceOf(Date);
  });

  it("creates ChangeDetail records linked to the parent AuditEntry", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) => run(trx, baseInput(), ctx));

    expect(result.ok).toBe(true);
    // First insert is AuditEntry, second is ChangeDetail
    expect(mock.inserts).toHaveLength(2);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.auditEntryId).toBe(baseAuditEntry.id);
    expect(changeValues.fieldName).toBe("amount");
  });

  it("applies MASK sensitivity mode to field values", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [activePolicy],
      [maskFieldRule],
      [baseAuditEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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 = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBe("******2345");
    expect(changeValues.newValue).toBe("******7890");
  });

  it("applies HASH sensitivity mode to field values", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [activePolicy],
      [hashFieldRule],
      [baseAuditEntry],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    // 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [activePolicy],
      [excludeFieldRule],
      [baseAuditEntry],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBeNull();
  });

  it("serializes scalar field values as strings", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    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 mock = createKyselyMock<Namespace["main-db"]>();
    const richtextEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "amount", fieldType: "richtext" }]),
    };
    mock.enqueueResults([], [richtextEntity], [activePolicy], [captureFieldRule], [baseAuditEntry]);

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBe("100");
    expect(changeValues.newValue).toBe("200");
  });

  it("serializes collection / array field values as JSON strings", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const arrayEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "tags", fieldType: "array" }]),
    };
    const arrayFieldRule = { ...captureFieldRule, fieldName: "tags" };
    mock.enqueueResults([], [arrayEntity], [activePolicy], [arrayFieldRule], [baseAuditEntry]);

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    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 mock = createKyselyMock<Namespace["main-db"]>();
    // customerId is registered as "relation" in fixtures
    const relationPolicy = { ...activePolicy };
    const relationFieldRule = { ...captureFieldRule, fieldName: "customerId" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [relationPolicy],
      [relationFieldRule],
      [baseAuditEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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 = mock.inserts[1].insertRows()[0];
    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 mock = createKyselyMock<Namespace["main-db"]>();
    const fileEntity = {
      ...companyBoundEntity,
      auditableFields: JSON.stringify([{ fieldName: "attachment", fieldType: "file" }]),
    };
    const fileFieldRule = { ...captureFieldRule, fieldName: "attachment" };
    mock.enqueueResults([], [fileEntity], [activePolicy], [fileFieldRule], [baseAuditEntry]);

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.newValue).toBe(
      JSON.stringify({ fileId: "f-1", filename: "doc.pdf", size: 1024 }),
    );
  });

  it("excludes binary / BLOB fields from audit capture", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // 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" },
    ];
    mock.enqueueResults([], [binaryEntity], [activePolicy], binaryFieldRules, [baseAuditEntry]);

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

    expect(result.ok).toBe(true);
    // Only 2 inserts: AuditEntry + 1 ChangeDetail (binaryData dropped by fieldType filter)
    expect(mock.inserts).toHaveLength(2);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.fieldName).toBe("amount");
  });

  it("excludes computed / derived fields from audit capture", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // 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" },
    ];
    mock.enqueueResults([], [computedEntity], [activePolicy], computedFieldRules, [baseAuditEntry]);

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

    expect(result.ok).toBe(true);
    // Only 2 inserts: AuditEntry + 1 ChangeDetail (computedTotal dropped by fieldType filter)
    expect(mock.inserts).toHaveLength(2);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.fieldName).toBe("amount");
  });

  it("silently drops fields from changes array that are not in entity's registered auditableFields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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(mock.inserts).toHaveLength(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 mock = createKyselyMock<Namespace["main-db"]>();
    // Only captureFieldRule for "amount" — "status" has no rule
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [activePolicy],
      [captureFieldRule],
      [baseAuditEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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(mock.inserts).toHaveLength(2);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.fieldName).toBe("amount");
  });

  it("truncates values exceeding 4,000 characters with [truncated] suffix", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.newValue).toHaveLength(4000);
    expect(changeValues.newValue).toMatch(/\[truncated\]$/);
  });

  it("stores correlationId when provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const correlationId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
    const result = await mock.withTx((trx) => run(trx, baseInput({ correlationId }), ctx));

    expect(result.ok).toBe(true);
    const entryValues = mock.inserts[0].insertValues();
    expect(entryValues.correlationId).toBe(correlationId);
  });

  it("stores actorMetadata (ipAddress, userAgent, sessionId, requestId) when provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

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

    expect(result.ok).toBe(true);
    const entryValues = mock.inserts[0].insertValues();
    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 mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) => run(trx, baseInput({ onBehalfOf: "user-3" }), ctx));

    expect(result.ok).toBe(true);
    const entryValues = mock.inserts[0].insertValues();
    expect(entryValues.onBehalfOf).toBe("user-3");
  });

  it("creates ChangeDetail with null oldValue for CREATE operations", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [createPolicy],
      [captureFieldRule],
      [{ ...baseAuditEntry, operationType: "CREATE" }],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBe("100.00");
  });

  it("creates ChangeDetail with null newValue for DELETE operations", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [deletePolicy],
      [captureFieldRule],
      [{ ...baseAuditEntry, operationType: "DELETE" }],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBe("100.00");
    expect(changeValues.newValue).toBeNull();
  });

  it("filters out unchanged fields for UPDATE operations", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    setupHappyPath(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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(mock.inserts).toHaveLength(2);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.fieldName).toBe("amount");
  });

  it("returns no-op when all UPDATE changes are unchanged after normalization", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [activePolicy],
      [captureFieldRule],
      [baseAuditEntry],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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(mock.inserts).toHaveLength(0);
  });

  it("enforces oldValue=null for CREATE even if caller provides oldValue", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [createPolicy],
      [captureFieldRule],
      [{ ...baseAuditEntry, operationType: "CREATE" }],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBeNull();
    expect(changeValues.newValue).toBe("100.00");
  });

  it("enforces newValue=null for DELETE even if caller provides newValue", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    mock.enqueueResults(
      [],
      [companyBoundEntity],
      [deletePolicy],
      [captureFieldRule],
      [{ ...baseAuditEntry, operationType: "DELETE" }],
    );

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

    expect(result.ok).toBe(true);
    const changeValues = mock.inserts[1].insertRows()[0];
    expect(changeValues.oldValue).toBe("100.00");
    expect(changeValues.newValue).toBeNull();
  });

  it("rejects event with empty entityId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]);

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([], [companyBoundEntity]); // no duplicate, entity found

    const result = await mock.withTx((trx) =>
      run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) => run(trx, 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 mock = createKyselyMock<Namespace["main-db"]>();
    const createPolicy = { ...activePolicy, operationType: "CREATE" };
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    mock.enqueueResults([], [companyBoundEntity], [createPolicy], [captureFieldRule]);

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

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

  it("rejects DELETE when all changes are filtered out by policy rules", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const deletePolicy = { ...activePolicy, operationType: "DELETE" };
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    mock.enqueueResults([], [companyBoundEntity], [deletePolicy], [captureFieldRule]);

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

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

  it("returns no-op for UPDATE when all changes are filtered out by policy rules", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // Only captureFieldRule for "amount" — submitted field "status" has no rule
    mock.enqueueResults([], [companyBoundEntity], [activePolicy], [captureFieldRule]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        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(mock.inserts).toHaveLength(0);
  });

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

    const result = await mock.withTx((trx) => command(trx, baseInput(), noPermCtx));

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