import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, it, expect, vi } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  EmptyAmendmentChangesError,
  PoNotFoundError,
  PoNotOrderedError,
  LineNotFoundError,
  ModifyQuantityBelowReceivedError,
  ModifyQuantityBelowBilledError,
  RemoveLineHasReceiptsError,
  RemoveLineHasBillsError,
  EmptyPurchaseOrderLinesError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  ReceivingSiteRequiredError,
} from "../lib/errors.generated";
import { commandCtx, expectErr } from "../testing/commandTestUtils";
import {
  baseItem,
  baseReceivingSite,
  baseUnit,
  baseReceiptEvidence,
  orderedPurchaseOrderWithRevision,
  poLineForAmendment,
} from "../testing/fixtures";
import {
  mockInventoryCommands,
  mockInventoryQueries,
  mockItemQueries,
  mockOrgQueries,
} from "../testing/moduleMocks";
import { run as runCommand } from "./amendOrderedPurchaseOrder";

type KMock = ReturnType<typeof createKyselyMock<Namespace["main-db"]>>;

function run(
  mock: KMock,
  input: Parameters<typeof runCommand>[1],
  ctx: Parameters<typeof runCommand>[2],
  itemManagementQueries: Parameters<typeof runCommand>[3],
  inventoryCommands: Parameters<typeof runCommand>[4] = mockInventoryCommands(),
  inventoryQueries: Parameters<typeof runCommand>[5] = mockInventoryQueries(),
  organizationQueries: Parameters<typeof runCommand>[6] = mockOrgQueries({
    site: baseReceivingSite,
  }),
) {
  return mock.withTx((trx) =>
    runCommand(
      trx,
      input,
      ctx,
      itemManagementQueries,
      inventoryCommands,
      inventoryQueries,
      organizationQueries,
    ),
  );
}

const po = orderedPurchaseOrderWithRevision;
const poLine = poLineForAmendment;

// Test fixture: envelope row returned from the insert with returningAll().
const envelopeReturn = {
  id: "po-revision-1",
  purchaseOrderId: po.id,
  revisionNumber: 1,
  reason: null,
  amendedByUserId: commandCtx.actorId,
  createdAt: new Date(),
  updatedAt: null,
};

describe("amendOrderedPurchaseOrder", () => {
  it("amends a PO with an UPDATE action (line ID unchanged, values updated)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [po], // PurchaseOrder (forUpdate)
      [poLine], // PurchaseOrderLine
      [po], // PurchaseOrder update (returningAll)
      [], // PurchaseOrderLine update
      [], // lastRevision
      [envelopeReturn], // PurchaseOrderRevision insert (returningAll)
      [], // PurchaseOrderFieldChange batch insert
    );

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Price change",
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8", unitPrice: "90" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
  });

  it("updates the unique inventory supply plan for a modified PO line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const inventoryCommands = mockInventoryCommands();
    const updateSupplySpy = vi.fn(inventoryCommands.updateInventorySupplyPlan);
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      {
        ...inventoryCommands,
        updateInventorySupplyPlan: updateSupplySpy,
      },
      mockInventoryQueries({ supplyPlans: [{ id: "supply-plan-1", sourceLineId: poLine.id }] }),
    );

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(updateSupplySpy).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        id: "supply-plan-1",
        sourceType: "PURCHASE_ORDER",
        sourceId: po.id,
        sourceLineId: poLine.id,
        expectedQuantity: "8",
      }),
      commandCtx,
    );
  });

  it("zeros the existing supply plan when an active PO line no longer requires physical receipt", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const inventoryCommands = mockInventoryCommands();
    const updateSupplySpy = vi.fn(inventoryCommands.updateInventorySupplyPlan);
    const closeSupplySpy = vi.fn(inventoryCommands.closeInventorySupplyPlan);
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { requiresPhysicalReceipt: false } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      {
        ...inventoryCommands,
        updateInventorySupplyPlan: updateSupplySpy,
        closeInventorySupplyPlan: closeSupplySpy,
      },
      mockInventoryQueries({ supplyPlans: [{ id: "supply-plan-1", sourceLineId: poLine.id }] }),
    );

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(updateSupplySpy).toHaveBeenCalledWith(
      expect.anything(),
      { id: "supply-plan-1", expectedQuantity: "0" },
      commandCtx,
    );
    expect(closeSupplySpy).not.toHaveBeenCalled();
  });

  it("amends a PO with a REMOVE action (line physically deleted)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const survivingLine = { ...poLine, id: "po-line-surviving" };
    mock.enqueueResults(
      [po],
      [poLine, survivingLine],
      [po], // PurchaseOrder update
      [], // PurchaseOrderLine delete
      [], // lastRevision
      [envelopeReturn], // envelope
      [], // field changes
    );

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Line no longer needed",
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
  });

  it("amends a PO with an ADD action (new line created, no line receivingSiteId — header is used)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newLine = {
      id: "new-po-line-1",
      purchaseOrderId: po.id,
      itemId: baseItem.id,
      itemSnapshotName: baseItem.name,
      itemSnapshotSku: baseItem.sku,
      quantity: "5",
      unitPrice: "50",
      unitId: baseUnit.id,
      receivingSiteId: null,
      requiresPhysicalReceipt: true,
      createdAt: new Date(),
      updatedAt: null,
    };
    mock.enqueueResults(
      [po],
      [poLine],
      [po], // PurchaseOrder update (header)
      [newLine], // PurchaseOrderLine insert (bulk)
      [], // lastRevision
      [envelopeReturn], // envelope
      [], // field changes
    );

    const result = await run(
      mock,
      {
        id: po.id,
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "5",
            unitPrice: "50",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
  });

  it("writes one PurchaseOrderRevision envelope per amendment", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        reason: "Adjust quantity",
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        purchaseOrderId: po.id,
        revisionNumber: 1,
        reason: "Adjust quantity",
        amendedByUserId: commandCtx.actorId,
      }),
    );
  });

  it("writes PurchaseOrderFieldChange rows for line UPDATE (only changed fields)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    // The field-change batch insert call: only `quantity` actually changed
    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          revisionId: envelopeReturn.id,
          recordType: "LINE",
          recordId: poLine.id,
          fieldName: "quantity",
          changeKind: "MODIFIED",
          oldValue: poLine.quantity,
          newValue: "8",
        }),
      ]),
    );
  });

  it("writes PurchaseOrderFieldChange rows for line REMOVE (one per non-null snapshot field)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const survivingLine = { ...poLine, id: "po-line-surviving" };
    mock.enqueueResults(
      [po],
      [poLine, survivingLine],
      [po], // PurchaseOrder update (header)
      [], // delete
      [],
      [envelopeReturn],
      [],
    );

    await run(
      mock,
      {
        id: po.id,
        reason: "Remove line",
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "LINE",
          recordId: poLine.id,
          fieldName: "quantity",
          changeKind: "REMOVED",
          oldValue: poLine.quantity,
          newValue: null,
        }),
        expect.objectContaining({
          fieldName: "unitPrice",
          changeKind: "REMOVED",
          newValue: null,
        }),
      ]),
    );
  });

  it("writes PurchaseOrderFieldChange rows for line ADD (one per non-null snapshot field)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newLine = {
      id: "new-po-line-1",
      purchaseOrderId: po.id,
      itemId: baseItem.id,
      itemSnapshotName: baseItem.name,
      itemSnapshotSku: baseItem.sku,
      quantity: "5",
      unitPrice: "50",
      unitId: baseUnit.id,
      receivingSiteId: null,
      requiresPhysicalReceipt: true,
      createdAt: new Date(),
      updatedAt: null,
    };
    mock.enqueueResults(
      [po],
      [poLine],
      [po], // PurchaseOrder update (header)
      [newLine], // ADD insert
      [],
      [envelopeReturn],
      [],
    );

    await run(
      mock,
      {
        id: po.id,
        reason: "Add new line",
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "5",
            unitPrice: "50",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[2].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "LINE",
          recordId: newLine.id,
          fieldName: "quantity",
          changeKind: "ADDED",
          oldValue: null,
          newValue: "5",
        }),
        expect.objectContaining({
          fieldName: "unitPrice",
          changeKind: "ADDED",
          oldValue: null,
          newValue: "50",
        }),
      ]),
    );
  });

  it("amends a PO header field (e.g. extension field) and records a HEADER MODIFIED row", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poWithExt = {
      ...po,
      // Simulate an extension field on the live header
      ...({ exFactoryDate: "2026-05-01" } as Record<string, unknown>),
    };
    mock.enqueueResults(
      [poWithExt],
      [poLine],
      // No UPDATE/REMOVE → skip received/billed lookups
      [poWithExt], // PurchaseOrder update
      [], // lastRevision
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Ex-factory delay",
        headerPatch: { exFactoryDate: "2026-05-15" },
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "HEADER",
          recordId: po.id,
          fieldName: "exFactoryDate",
          changeKind: "MODIFIED",
          oldValue: "2026-05-01",
          newValue: "2026-05-15",
        }),
      ]),
    );
  });

  it("amends receivingSiteId as a validated header default", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newReceivingSite = { ...baseReceivingSite, id: "site-2" };
    mock.enqueueResults(
      [po],
      [poLine],
      [{ ...po, receivingSiteId: newReceivingSite.id }], // PurchaseOrder update
      [], // lastRevision
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        headerPatch: { receivingSiteId: newReceivingSite.id },
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({ receivingSiteId: newReceivingSite.id }),
    );
    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "HEADER",
          recordId: po.id,
          fieldName: "receivingSiteId",
          changeKind: "MODIFIED",
          oldValue: po.receivingSiteId,
          newValue: newReceivingSite.id,
        }),
      ]),
    );
  });

  it("increments envelope revisionNumber from the existing maximum", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [po],
      [poLine],
      [po], // PurchaseOrder update (header)
      [], // line update
      [{ revisionNumber: 3 }], // existing top revisionNumber
      [envelopeReturn],
      [],
    );

    await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        purchaseOrderId: po.id,
        revisionNumber: 4,
      }),
    );
  });

  it("recalculates receiptStatus after amendment", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [po],
      [poLine],
      [{ ...po, receiptStatus: "PARTIALLY_RECEIVED" }], // PurchaseOrder update (header)
      [], // line update
      [], // lastRevision
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { unitPrice: "90" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      mockInventoryCommands(),
      mockInventoryQueries({
        receiptEvidence: [{ ...baseReceiptEvidence, purchaseOrderLineId: poLine.id }],
      }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseOrder.receiptStatus).toBe("PARTIALLY_RECEIVED");
  });

  it("recalculates billingStatus after amendment", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const billedLine = { ...poLine, billedQuantity: "4" };
    mock.enqueueResults(
      [po],
      [billedLine],
      [{ ...po, billingStatus: "PARTIALLY_BILLED" }], // PurchaseOrder update (header)
      [], // line update
      [], // lastRevision
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { unitPrice: "90" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseOrder.billingStatus).toBe("PARTIALLY_BILLED");
  });

  it("supports price-only amendment (quantity not provided)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { unitPrice: "120" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    // Only unitPrice is written; quantity is left untouched (preserved at its current value).
    expect(mock.updates[1].updateValues()).toEqual({ unitPrice: "120" });
  });

  it("returns error when neither headerPatch nor line changes are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    // Status recalc + the header write run before the empty-change check rejects (txn rolls back).
    mock.enqueueResults([po], [poLine], [po]);

    const result = await run(mock, { id: po.id }, commandCtx, mockItemQueries({ item: baseItem }));
    expectErr(result, EmptyAmendmentChangesError);
  });

  it("returns error when the amendment contains no actual field changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine], [po]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: {} }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expectErr(result, EmptyAmendmentChangesError);
    // The header write (status recalc) runs before the empty-change check; no revision is persisted.
    expect(mock.updates).toHaveLength(1);
    expect(mock.inserts).toHaveLength(0);
  });

  it("returns error when PO does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await run(
      mock,
      { id: "missing-po" },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, PoNotFoundError);
  });

  it("returns error when PO is not ORDERED", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([{ ...po, orderStatus: "DRAFT" }]);

    const result = await run(mock, { id: po.id }, commandCtx, mockItemQueries({ item: baseItem }));
    expectErr(result, PoNotOrderedError);
  });

  it("returns error when UPDATE references a non-existent line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: "missing-line", linePatch: { quantity: "5" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, LineNotFoundError);
  });

  it("returns error when UPDATE reduces quantity below received", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "5" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      mockInventoryCommands(),
      mockInventoryQueries({
        receiptEvidence: [
          { ...baseReceiptEvidence, purchaseOrderLineId: poLine.id, quantity: "8" },
        ],
      }),
    );
    expectErr(result, ModifyQuantityBelowReceivedError);
  });

  it("returns error when UPDATE reduces quantity below billed", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [{ ...poLine, billedQuantity: "8" }]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "5" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, ModifyQuantityBelowBilledError);
  });

  it("returns error when UPDATE reduces quantity below posted + draft received", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "6" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      mockInventoryCommands(),
      mockInventoryQueries({
        receiptEvidence: [
          { ...baseReceiptEvidence, purchaseOrderLineId: poLine.id, quantity: "3" },
          { ...baseReceiptEvidence, purchaseOrderLineId: poLine.id, quantity: "5" },
        ],
      }),
    );
    expectErr(result, ModifyQuantityBelowReceivedError);
  });

  it("returns error when REMOVE targets a line with posted receipts", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      mockInventoryCommands(),
      mockInventoryQueries({
        receiptEvidence: [{ ...baseReceiptEvidence, purchaseOrderLineId: poLine.id }],
      }),
    );
    expectErr(result, RemoveLineHasReceiptsError);
  });

  it("returns error when REMOVE targets a line with billed quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [{ ...poLine, billedQuantity: "4" }]);

    const result = await run(
      mock,
      {
        id: po.id,
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, RemoveLineHasBillsError);
  });

  it("returns error when REMOVE targets a line referenced by draft downstream documents", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
      mockInventoryCommands(),
      // Draft receipt evidence (quantity 0 avoids the received-quantity guard) still blocks removal.
      mockInventoryQueries({
        receiptEvidence: [
          { ...baseReceiptEvidence, purchaseOrderLineId: poLine.id, quantity: "0" },
        ],
      }),
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect((result.error as Error).name).toBe("RemoveLineHasDownstreamReferencesError");
    }
  });

  it("returns error when ADD has zero or negative quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "0",
            unitPrice: "50",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, InvalidQuantityError);
  });

  it("returns error when amendment removes all remaining lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, EmptyPurchaseOrderLinesError);
  });

  it("returns error when ADD references a non-existent item", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        addLines: [
          {
            itemId: "non-existent-item",
            quantity: "5",
            unitPrice: "50",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: null }),
    );
    expectErr(result, ItemNotFoundError);
  });

  it("amends a PO line with extension fields via UPDATE and records LINE MODIFIED rows for changed extension fields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineWithExt = {
      ...poLine,
      ...({ note: "old note", priority: "LOW" } as Record<string, unknown>),
    };
    mock.enqueueResults([po], [poLineWithExt], [po], [], [], [envelopeReturn], []);

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Update note",
        updateLines: [{ lineId: poLine.id, linePatch: { note: "new note", priority: "LOW" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    expect(mock.updates[1].updateValues()).toEqual(expect.objectContaining({ note: "new note" }));
    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "LINE",
          recordId: poLine.id,
          fieldName: "note",
          changeKind: "MODIFIED",
          oldValue: "old note",
          newValue: "new note",
        }),
      ]),
    );
    // priority did not change → no MODIFIED row for it
    expect(mock.inserts[1].insertRows()).not.toEqual(
      expect.arrayContaining([expect.objectContaining({ fieldName: "priority" })]),
    );
  });

  it("amends a PO line with extension fields via ADD and records LINE ADDED rows for non-null extension fields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const newLine = {
      id: "new-po-line-1",
      purchaseOrderId: po.id,
      itemId: baseItem.id,
      itemSnapshotName: baseItem.name,
      itemSnapshotSku: baseItem.sku,
      quantity: "5",
      unitPrice: "50",
      unitId: baseUnit.id,
      receivingSiteId: null,
      requiresPhysicalReceipt: true,
      note: "added note",
      priority: "HIGH",
      createdAt: new Date(),
      updatedAt: null,
    };
    mock.enqueueResults(
      [po],
      [poLine],
      [po], // PurchaseOrder update (header)
      [newLine], // ADD insert
      [],
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Add line with notes",
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "5",
            unitPrice: "50",
            unitId: baseUnit.id,
            note: "added note",
            priority: "HIGH",
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    expect(mock.inserts[0].insertRows()).toEqual(
      expect.arrayContaining([expect.objectContaining({ note: "added note", priority: "HIGH" })]),
    );
    expect(mock.inserts[2].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "LINE",
          recordId: newLine.id,
          fieldName: "note",
          changeKind: "ADDED",
          oldValue: null,
          newValue: "added note",
        }),
        expect.objectContaining({
          fieldName: "priority",
          changeKind: "ADDED",
          newValue: "HIGH",
        }),
      ]),
    );
  });

  it("writes PurchaseOrderFieldChange rows for line REMOVE including non-null extension fields", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineWithExt = {
      ...poLine,
      ...({ note: "removed note", priority: "HIGH", obsolete: null } as Record<string, unknown>),
    };
    const survivingLine = { ...poLine, id: "po-line-surviving" };
    mock.enqueueResults(
      [po],
      [poLineWithExt, survivingLine],
      [po], // PurchaseOrder update (header)
      [], // delete
      [],
      [envelopeReturn],
      [],
    );

    await run(
      mock,
      {
        id: po.id,
        reason: "Remove line with extensions",
        removeLineIds: [poLine.id],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          fieldName: "note",
          changeKind: "REMOVED",
          oldValue: "removed note",
          newValue: null,
        }),
        expect.objectContaining({
          fieldName: "priority",
          changeKind: "REMOVED",
          oldValue: "HIGH",
          newValue: null,
        }),
      ]),
    );
    // null-valued extension fields should not appear
    expect(mock.inserts[1].insertRows()).not.toEqual(
      expect.arrayContaining([expect.objectContaining({ fieldName: "obsolete" })]),
    );
  });

  it("amends a PO line with explicit clear of an extension field via UPDATE (null value)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineWithExt = {
      ...poLine,
      ...({ note: "old note" } as Record<string, unknown>),
    };
    mock.enqueueResults([po], [poLineWithExt], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        reason: "Clear note",
        updateLines: [{ lineId: poLine.id, linePatch: { note: null } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.updates[1].updateValues()).toEqual(expect.objectContaining({ note: null }));
    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          fieldName: "note",
          changeKind: "MODIFIED",
          oldValue: "old note",
          newValue: null,
        }),
      ]),
    );
  });

  it("skips a MODIFIED row for a line UPDATE whose extension field value is unchanged", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineWithExt = {
      ...poLine,
      ...({ note: "same" } as Record<string, unknown>),
    };
    mock.enqueueResults([po], [poLineWithExt], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        // quantity changes (so the amendment is not empty), note is provided but unchanged
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8", note: "same" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[1].insertRows()).not.toEqual(
      expect.arrayContaining([expect.objectContaining({ fieldName: "note" })]),
    );
  });

  it("supports combining header, UPDATE, ADD, and REMOVE in a single amendment", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const otherPoLine = { ...poLine, id: "po-line-other" };
    const newLine = {
      id: "new-po-line-2",
      purchaseOrderId: po.id,
      itemId: baseItem.id,
      itemSnapshotName: baseItem.name,
      itemSnapshotSku: baseItem.sku,
      quantity: "3",
      unitPrice: "30",
      unitId: baseUnit.id,
      receivingSiteId: null,
      requiresPhysicalReceipt: true,
      createdAt: new Date(),
      updatedAt: null,
    };
    mock.enqueueResults(
      [po], // PurchaseOrder forUpdate
      [poLine, otherPoLine], // PurchaseOrderLines
      [po], // PurchaseOrder update (header)
      [], // UPDATE line update
      [], // delete (REMOVE)
      [newLine], // ADD insert
      [], // lastRevision
      [envelopeReturn], // envelope
      [], // field changes
    );

    const result = await run(
      mock,
      {
        id: po.id,
        reason: "Bundled amendment",
        headerPatch: { externalSupplierOrderReference: "PO-EXT-NEW" },
        updateLines: [{ lineId: poLine.id, linePatch: { unitPrice: "120" } }],
        removeLineIds: [otherPoLine.id],
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "3",
            unitPrice: "30",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    // inserts: [0] ADD line, [1] envelope, [2] field changes
    expect(mock.inserts[2].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "HEADER",
          fieldName: "externalSupplierOrderReference",
          changeKind: "MODIFIED",
          newValue: "PO-EXT-NEW",
        }),
        expect.objectContaining({
          recordType: "LINE",
          recordId: poLine.id,
          fieldName: "unitPrice",
          changeKind: "MODIFIED",
          newValue: "120",
        }),
        expect.objectContaining({
          recordType: "LINE",
          recordId: otherPoLine.id,
          changeKind: "REMOVED",
        }),
        expect.objectContaining({
          recordType: "LINE",
          recordId: newLine.id,
          changeKind: "ADDED",
        }),
      ]),
    );
  });

  it("returns error when UPDATE has zero or negative quantity", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "0" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, InvalidQuantityError);
  });

  it("returns error when UPDATE has negative unitPrice", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { unitPrice: "-1" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, InvalidUnitPriceError);
  });

  it("returns error when UPDATE would leave a physical-receipt line without a receiving site", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poNoSite = { ...po, receivingSiteId: null };
    const poLineNoSite = { ...poLine, receivingSiteId: null, requiresPhysicalReceipt: true };
    mock.enqueueResults([poNoSite], [poLineNoSite]);

    const result = await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { quantity: "8" } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, ReceivingSiteRequiredError);
  });

  it("returns error when ADD would create a physical-receipt line without a receiving site", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poNoSite = { ...po, receivingSiteId: null };
    mock.enqueueResults([poNoSite], [poLine]);

    const result = await run(
      mock,
      {
        id: po.id,
        addLines: [
          {
            itemId: baseItem.id,
            quantity: "5",
            unitPrice: "50",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, ReceivingSiteRequiredError);
  });

  it("returns error when clearing header receivingSiteId leaves a remaining physical-receipt line without one", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineNoSite = { ...poLine, receivingSiteId: null, requiresPhysicalReceipt: true };
    mock.enqueueResults([po], [poLineNoSite]);

    const result = await run(
      mock,
      {
        id: po.id,
        headerPatch: { receivingSiteId: null },
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expectErr(result, ReceivingSiteRequiredError);
  });

  it("writes ISO date strings in audit rows when header orderDate changes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const oldDate = po.orderDate;
    const newDate = new Date("2026-06-01T00:00:00.000Z");
    mock.enqueueResults(
      [po],
      [poLine],
      [{ ...po, orderDate: newDate }], // PurchaseOrder update
      [], // lastRevision
      [envelopeReturn],
      [],
    );

    const result = await run(
      mock,
      {
        id: po.id,
        headerPatch: { orderDate: newDate },
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          recordType: "HEADER",
          fieldName: "orderDate",
          changeKind: "MODIFIED",
          oldValue: oldDate.toISOString(),
          newValue: newDate.toISOString(),
        }),
      ]),
    );
  });

  it("writes boolean strings in audit rows when line UPDATE toggles requiresPhysicalReceipt", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([po], [poLine], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { requiresPhysicalReceipt: false } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          fieldName: "requiresPhysicalReceipt",
          changeKind: "MODIFIED",
          oldValue: "true",
          newValue: "false",
        }),
      ]),
    );
  });

  it("serializes numeric extension field values in line UPDATE audit rows", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const poLineWithRank = {
      ...poLine,
      ...({ priorityRank: 5 } as Record<string, unknown>),
    };
    mock.enqueueResults([po], [poLineWithRank], [po], [], [], [envelopeReturn], []);

    await run(
      mock,
      {
        id: po.id,
        updateLines: [{ lineId: poLine.id, linePatch: { priorityRank: 10 } }],
      },
      commandCtx,
      mockItemQueries({ item: baseItem }),
    );

    expect(mock.inserts[1].insertRows()).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          fieldName: "priorityRank",
          changeKind: "MODIFIED",
          oldValue: "5",
          newValue: "10",
        }),
      ]),
    );
  });
});
