import { describe, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CloseReasonRequiredError,
  OpenQuantityRemainsError,
  PurchaseOrderNotClosableError,
  PurchaseOrderNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { basePurchaseOrder, basePurchaseOrderLine, postedGoodsReceipt } from "../testing/fixtures";
import { run } from "./closePurchaseOrder";

describe("closePurchaseOrder", () => {
  it("closes a fully billed purchase order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billedOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "RECEIVED",
      billingStatus: "BILLED",
    };
    const fullyBilledLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: billedOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: fullyBilledLine.id,
      quantity: 10,
    };
    const billLine = {
      id: "bill-line-1",
      purchaseBillId: "matched-bill-1",
      purchaseOrderLineId: fullyBilledLine.id,
      quantity: 10,
    };
    spies.select
      .mockReturnValueOnce(billedOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([fullyBilledLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([billLine]); // PurchaseBillLine JOIN PurchaseBill (MATCHED)
    spies.update.mockReturnValueOnce({ ...billedOrder, orderStatus: "CLOSED" });
    expectOk(await run(db, { id: billedOrder.id }, commandCtx));
  });

  it("closes a partially received order when remaining quantity is explicitly written off", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partialOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "PARTIALLY_RECEIVED",
      billingStatus: "NOT_BILLED",
    };
    const partialLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: partialOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: partialLine.id,
      quantity: 4,
    };
    spies.select
      .mockReturnValueOnce(partialOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([partialLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([]); // PurchaseBillLine JOIN PurchaseBill (MATCHED) — none
    spies.update.mockReturnValueOnce({
      ...partialOrder,
      orderStatus: "CLOSED",
      closeReason: "Write off",
    });
    expectOk(
      await run(
        db,
        { id: partialOrder.id, writeOffRemaining: true, closeReason: "Write off" },
        commandCtx,
      ),
    );
  });

  it("returns error when purchase order does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    expectErr(await run(db, { id: "missing-order" }, commandCtx), PurchaseOrderNotFoundError);
  });

  it("returns error when purchase order status does not allow closure", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseOrder);
    expectErr(
      await run(db, { id: basePurchaseOrder.id }, commandCtx),
      PurchaseOrderNotClosableError,
    );
  });

  it("returns error when unresolved open quantity remains", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partialOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "PARTIALLY_RECEIVED",
      billingStatus: "NOT_BILLED",
    };
    const partialLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: partialOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: partialLine.id,
      quantity: 4,
    };
    spies.select
      .mockReturnValueOnce(partialOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([partialLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([]); // PurchaseBillLine JOIN PurchaseBill (MATCHED) — none
    expectErr(await run(db, { id: partialOrder.id }, commandCtx), OpenQuantityRemainsError);
  });

  it("returns error when residual quantity exists without a close reason", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partialOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "PARTIALLY_RECEIVED",
      billingStatus: "NOT_BILLED",
    };
    const partialLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: partialOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: partialLine.id,
      quantity: 4,
    };
    spies.select
      .mockReturnValueOnce(partialOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([partialLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([]); // PurchaseBillLine JOIN PurchaseBill (MATCHED) — none
    expectErr(
      await run(db, { id: partialOrder.id, writeOffRemaining: true }, commandCtx),
      CloseReasonRequiredError,
    );
  });

  it("allows closure without write-off when each line is fully received", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receivedOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "RECEIVED",
      billingStatus: "PARTIALLY_BILLED",
    };
    const fullyReceivedLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: receivedOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: fullyReceivedLine.id,
      quantity: 10,
    };
    spies.select
      .mockReturnValueOnce(receivedOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([fullyReceivedLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([]); // PurchaseBillLine JOIN PurchaseBill (MATCHED) — none
    spies.update.mockReturnValueOnce({ ...receivedOrder, orderStatus: "CLOSED" });

    expectOk(await run(db, { id: receivedOrder.id }, commandCtx));
  });

  it("returns OPEN_QUANTITY_REMAINS before requiring a close reason when write-off is not requested", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partialOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "PARTIALLY_RECEIVED",
      billingStatus: "NOT_BILLED",
    };
    const partialLine = {
      ...basePurchaseOrderLine,
      purchaseOrderId: partialOrder.id,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: partialLine.id,
      quantity: 4,
    };
    spies.select
      .mockReturnValueOnce(partialOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([partialLine]) // selectFrom PurchaseOrderLine
      .mockReturnValueOnce([grLine]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([]); // PurchaseBillLine JOIN PurchaseBill (MATCHED) — none

    expectErr(
      await run(db, { id: partialOrder.id, closeReason: "Attempted close" }, commandCtx),
      OpenQuantityRemainsError,
    );
  });

  it("closes an order with no lines (empty PO line IDs skip GR/Bill line queries)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const emptyOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
      receiptStatus: "NOT_RECEIVED",
      billingStatus: "NOT_BILLED",
    };
    spies.select
      .mockReturnValueOnce(emptyOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([]); // PurchaseOrderLine (empty → GR/Bill queries skipped)
    spies.update.mockReturnValueOnce({ ...emptyOrder, orderStatus: "CLOSED" });

    expectOk(await run(db, { id: emptyOrder.id }, commandCtx));
  });

  it("sums received and billed quantities across multiple GR/bill lines per PO line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const orderedOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
    };
    const line = {
      ...basePurchaseOrderLine,
      purchaseOrderId: orderedOrder.id,
      quantity: "10",
    };
    // Two GR lines that sum to the ordered quantity
    const grLine1 = {
      id: "gr-line-1",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: line.id,
      quantity: 6,
    };
    const grLine2 = {
      id: "gr-line-2",
      goodsReceiptId: postedGoodsReceipt.id,
      purchaseOrderLineId: line.id,
      quantity: 4,
    };
    // Bill line covering full quantity
    const billLine = {
      id: "bill-line-1",
      purchaseBillId: "matched-bill-1",
      purchaseOrderLineId: line.id,
      quantity: 10,
    };
    spies.select
      .mockReturnValueOnce(orderedOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([line]) // PurchaseOrderLine
      .mockReturnValueOnce([grLine1, grLine2]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED)
      .mockReturnValueOnce([billLine]); // PurchaseBillLine JOIN PurchaseBill (MATCHED)
    spies.update.mockReturnValueOnce({ ...orderedOrder, orderStatus: "CLOSED" });

    expectOk(await run(db, { id: orderedOrder.id }, commandCtx));
  });

  it("detects open quantity when only billed but not received", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const orderedOrder = {
      ...basePurchaseOrder,
      orderStatus: "ORDERED",
    };
    const line = {
      ...basePurchaseOrderLine,
      purchaseOrderId: orderedOrder.id,
      quantity: "10",
    };
    // Billed fully but not received at all
    const billLine = {
      id: "bill-line-1",
      purchaseBillId: "matched-bill-1",
      purchaseOrderLineId: line.id,
      quantity: 10,
    };
    spies.select
      .mockReturnValueOnce(orderedOrder) // selectOne PurchaseOrder (forUpdate)
      .mockReturnValueOnce([line]) // PurchaseOrderLine
      .mockReturnValueOnce([]) // GoodsReceiptLine JOIN GoodsReceipt (POSTED) — none
      .mockReturnValueOnce([billLine]); // PurchaseBillLine JOIN PurchaseBill (MATCHED)
    spies.update.mockReturnValueOnce({ ...orderedOrder, orderStatus: "CLOSED" });

    // received < quantity but billed >= quantity, so NOT hasUnresolvedOpenQuantity
    expectOk(await run(db, { id: orderedOrder.id }, commandCtx));
  });
});
