import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  EmptyPurchaseOrderLinesError,
  ItemNotActiveError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  LineNotFoundError,
  PurchaseOrderNotDraftError,
  PurchaseOrderNotFoundError,
  ReceivingSiteNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseItem,
  basePurchaseOrder,
  basePurchaseOrderLine,
  basePurchaseOrderLines,
  baseReceivingSite,
  inactiveItem,
} from "../testing/fixtures";
import { mockItemQueries, mockOrgQueries } from "../testing/moduleMocks";
import { run } from "./updatePurchaseOrder";

const itemQ = mockItemQueries({ item: baseItem });

const draftLine = {
  ...basePurchaseOrderLine,
  purchaseOrderId: basePurchaseOrder.id,
};

describe("updatePurchaseOrder", () => {
  it("updates supplier-facing data on a draft purchase order", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const updated = {
      ...basePurchaseOrder,
      externalSupplierOrderReference: "PO-EXT-2",
    };
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
      [updated], // update PurchaseOrder
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseOrder.id,
          headerPatch: { externalSupplierOrderReference: "PO-EXT-2" },
        },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(expectOk(result).purchaseOrder.externalSupplierOrderReference).toBe("PO-EXT-2");
  });

  it("skips the header update when headerPatch is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: basePurchaseOrder.id, headerPatch: {} },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
    expect(mock.updates).toHaveLength(0);
  });

  it("skips a line update when its patch is empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { id: basePurchaseOrder.id, updateLines: [{ lineId: draftLine.id, linePatch: {} }] },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
    expect(mock.updates).toHaveLength(0);
  });

  it("returns error when purchase order does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);
    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { id: "missing-order", headerPatch: { orderDate: new Date() } },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      PurchaseOrderNotFoundError,
    );
  });

  it("returns error when purchase order is not in draft", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([{ ...basePurchaseOrder, orderStatus: "SUBMITTED" }]);
    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            headerPatch: { orderDate: new Date() },
          },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      PurchaseOrderNotDraftError,
    );
  });

  it("adds a line with item snapshot fields via an ADD change", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
      [], // insert line
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseOrder.id,
          addLines: [basePurchaseOrderLines[0]],
        },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()[0]).toEqual(
      expect.objectContaining({
        purchaseOrderId: basePurchaseOrder.id,
        itemId: baseItem.id,
        itemSnapshotName: baseItem.name,
        itemSnapshotSku: baseItem.sku,
      }),
    );
  });

  it("updates a line in place so its id and createdAt survive the edit", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
      [], // update line
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseOrder.id,
          updateLines: [{ lineId: draftLine.id, linePatch: { quantity: "20" } }],
        },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
    // No delete-and-reinsert: the untouched fields live on the same row. The
    // header is not patched, so only the line is updated.
    expect(mock.inserts).toHaveLength(0);
    expect(mock.updates[0].updateValues()).toEqual(expect.objectContaining({ quantity: "20" }));
  });

  it("removes one of multiple lines via a REMOVE change", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const secondLine = { ...draftLine, id: "po-line-2" };
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine, secondLine], // current lines
      [], // delete line
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: basePurchaseOrder.id,
          removeLineIds: [draftLine.id],
        },
        commandCtx,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
  });

  it("returns error when a REMOVE would leave the order without lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            removeLineIds: [draftLine.id],
          },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      EmptyPurchaseOrderLinesError,
    );
  });

  it("returns error when an UPDATE targets a missing line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            updateLines: [{ lineId: "missing-line", linePatch: { quantity: "20" } }],
          },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      LineNotFoundError,
    );
  });

  it("returns error when an updated line item does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            updateLines: [{ lineId: draftLine.id, linePatch: { itemId: "missing-item" } }],
          },
          commandCtx,
          mockItemQueries(),
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      ItemNotFoundError,
    );
  });

  it("returns error when an added line item is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            addLines: [{ ...basePurchaseOrderLines[0], itemId: inactiveItem.id }],
          },
          commandCtx,
          mockItemQueries({ item: inactiveItem }),
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      ItemNotActiveError,
    );
  });

  it("returns error when an updated line quantity is not positive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            updateLines: [{ lineId: draftLine.id, linePatch: { quantity: "0" } }],
          },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      InvalidQuantityError,
    );
  });

  it("returns error when an added line unit price is negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            addLines: [{ ...basePurchaseOrderLines[0], unitPrice: "-1" }],
          },
          commandCtx,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      InvalidUnitPriceError,
    );
  });

  it("returns error when header receiving site does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );
    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            headerPatch: { receivingSiteId: "missing-site" },
          },
          commandCtx,
          itemQ,
          mockOrgQueries(),
        ),
      ),
      ReceivingSiteNotFoundError,
    );
  });

  it("returns error when an updated line receiving site does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [basePurchaseOrder], // selectOne PurchaseOrder
      [draftLine], // current lines
    );

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            id: basePurchaseOrder.id,
            updateLines: [{ lineId: draftLine.id, linePatch: { receivingSiteId: "missing-site" } }],
          },
          commandCtx,
          itemQ,
          mockOrgQueries(),
        ),
      ),
      ReceivingSiteNotFoundError,
    );
  });
});
