import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  MatchModeRequiredError,
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotDraftError,
  PurchaseBillNotFoundError,
  PurchaseOrderCancelledError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseGoodsReceiptLine,
  basePurchaseBill,
  basePurchaseBillLine,
  basePurchaseOrderLine,
  matchedPurchaseBill,
  orderedPurchaseOrder,
  postedGoodsReceipt,
} from "../testing/fixtures";
import { run } from "./matchPurchaseBill";

describe("matchPurchaseBill", () => {
  it("matches a valid 2-way purchase bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder (per bill line)
    // no variance → update PurchaseBill then derive billingStatus
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" }); // update PurchaseBill
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([matchedPurchaseBill]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ status: "MATCHED", varianceReason: null }),
    );
  });

  it("matches a valid 3-way purchase bill when posted receipt quantity is sufficient", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedReceiptLine = { ...baseGoodsReceiptLine, goodsReceiptId: postedGoodsReceipt.id };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([postedGoodsReceipt]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder) // PurchaseOrder (per bill line)
      // planThreeWayMatch: GoodsReceiptLine (direct per PO line ID)
      .mockReturnValueOnce([postedReceiptLine]) // GoodsReceiptLine (for orderLine.id)
      .mockReturnValueOnce([]); // ThreeWayMatchEvent (existing matches)
    // ThreeWayMatchEvent insert
    spies.insert.mockReturnValueOnce(undefined);
    // no variance → update PurchaseBill then derive billingStatus
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "THREE_WAY" }); // update PurchaseBill
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([matchedPurchaseBill]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(
      db,
      { id: basePurchaseBill.id, matchingMode: "THREE_WAY" },
      commandCtx,
    );

    expect(expectOk(result).purchaseBill.matchingMode).toBe("THREE_WAY");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ status: "MATCHED", varianceReason: null }),
    );
  });

  it("blocks a bill when 3-way quantity exceeds posted receipt quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // Receipt line with quantity 1 (bill line has quantity 4, exceeds received)
    const smallReceiptLine = {
      ...baseGoodsReceiptLine,
      goodsReceiptId: postedGoodsReceipt.id,
      quantity: "1",
    };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([postedGoodsReceipt]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder) // PurchaseOrder (per bill line)
      // planThreeWayMatch: GoodsReceiptLine (direct per PO line ID)
      .mockReturnValueOnce([smallReceiptLine]) // GoodsReceiptLine - insufficient qty
      .mockReturnValueOnce([]); // ThreeWayMatchEvent (existing matches)
    // variance → no billed quantity updates
    spies.update.mockReturnValueOnce({
      ...basePurchaseBill,
      status: "BLOCKED",
      varianceReason: "RECEIPT_QUANTITY_EXCEEDED",
    }); // update PurchaseBill

    const result = await run(
      db,
      { id: basePurchaseBill.id, matchingMode: "THREE_WAY" },
      commandCtx,
    );

    expect(expectOk(result).purchaseBill.status).toBe("BLOCKED");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ status: "BLOCKED", varianceReason: "RECEIPT_QUANTITY_EXCEEDED" }),
    );
  });

  it("blocks a bill when price variance exceeds tolerance", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const highPriceBillLine = { ...basePurchaseBillLine, billedUnitPrice: 120 };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([highPriceBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder (per bill line)
    // variance → no billed quantity updates
    spies.update.mockReturnValueOnce({
      ...basePurchaseBill,
      status: "BLOCKED",
      varianceReason: "PRICE_VARIANCE_EXCEEDED",
    }); // update PurchaseBill

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("BLOCKED");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ status: "BLOCKED", varianceReason: "PRICE_VARIANCE_EXCEEDED" }),
    );
  });

  it("records matching mode on the bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder (per bill line)
    // no variance → update PurchaseBill then derive billingStatus
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" }); // update PurchaseBill
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([matchedPurchaseBill]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.matchingMode).toBe("TWO_WAY");
  });

  it("derives purchase-order billing status when the bill is matched", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder (per bill line)
    // no variance → update PurchaseBill then derive billingStatus
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" }); // update PurchaseBill
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([matchedPurchaseBill]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    // billingStatus is derived from PurchaseBillLine + MATCHED bills
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ billingStatus: expect.any(String) as string }),
    );
  });

  it("matches a purchase bill with PO-unlinked lines (skip PO variance checks, include in amount)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unlinkedBillLine = { ...basePurchaseBillLine, purchaseOrderLineId: null };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([unlinkedBillLine]) // PurchaseBillLine rows (PO-unlinked)
      .mockReturnValueOnce([]); // GoodsReceipt posted
    // No PO line/order lookups for unlinked lines
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" }); // update PurchaseBill

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");
  });

  it("creates ThreeWayMatchEvent records for bill-line-to-receipt-line correspondence", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const postedReceiptLine = { ...baseGoodsReceiptLine, goodsReceiptId: postedGoodsReceipt.id };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([postedGoodsReceipt]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder) // PurchaseOrder (per bill line)
      // planThreeWayMatch: GoodsReceiptLine (direct per PO line ID)
      .mockReturnValueOnce([postedReceiptLine]) // GoodsReceiptLine (for orderLine.id)
      .mockReturnValueOnce([]); // ThreeWayMatchEvent (existing matches)
    // ThreeWayMatchEvent insert (no return value needed)
    spies.insert.mockReturnValueOnce(undefined);
    // no variance → update PurchaseBill then derive billingStatus
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "THREE_WAY" }); // update PurchaseBill
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([matchedPurchaseBill]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(
      db,
      { id: basePurchaseBill.id, matchingMode: "THREE_WAY" },
      commandCtx,
    );

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");

    // Verify ThreeWayMatchEvent was inserted with correct bill-line-to-receipt-line mapping
    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        purchaseBillLineId: basePurchaseBillLine.id,
        goodsReceiptLineId: postedReceiptLine.id,
        matchedQuantity: basePurchaseBillLine.quantity,
      }),
    );
  });

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

  it("returns error when purchase bill is not draft", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(matchedPurchaseBill);
    expectErr(
      await run(db, { id: matchedPurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx),
      PurchaseBillNotDraftError,
    );
  });

  it("returns error when matching mode is missing", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseBill);
    expectErr(
      await run(db, { id: basePurchaseBill.id, matchingMode: undefined as never }, commandCtx),
      MatchModeRequiredError,
    );
  });

  it("returns error when a referenced purchase order is cancelled", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce({ ...orderedPurchaseOrder, orderStatus: "CANCELLED" }); // PurchaseOrder (cancelled)
    expectErr(
      await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx),
      PurchaseOrderCancelledError,
    );
  });

  it("returns error when payment schedule calculation fails", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const invalidScheduleBill = {
      ...basePurchaseBill,
      paymentTermSnapshotLines: [],
    };
    spies.select
      .mockReturnValueOnce(invalidScheduleBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine (getOrderLine per bill line)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder (per bill line)
    expectErr(
      await run(db, { id: invalidScheduleBill.id, matchingMode: "TWO_WAY" }, commandCtx),
      PaymentScheduleCalculationFailedError,
    );
  });

  it("detects price variance when ordered unit price is zero and billed is positive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const zeroUnitPriceOrderLine = { ...basePurchaseOrderLine, unitPrice: "0" };
    const positiveBilledBillLine = { ...basePurchaseBillLine, billedUnitPrice: "10" };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([positiveBilledBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(zeroUnitPriceOrderLine) // PurchaseOrderLine (getOrderLine)
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder
    spies.update.mockReturnValueOnce({
      ...basePurchaseBill,
      status: "BLOCKED",
      varianceReason: "PRICE_VARIANCE_EXCEEDED",
    });

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("BLOCKED");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ status: "BLOCKED", varianceReason: "PRICE_VARIANCE_EXCEEDED" }),
    );
  });

  it("skips bill lines when resolved order line is not found", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // A bill line references a PO line but getOrderLine returns undefined
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(undefined); // getOrderLine returns undefined (no order line found)
    // No PO lookup since orderLine is null → continue
    // calculateDueSchedule then update
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" });

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");
  });

  it("skips bill lines when purchase order for the order line is not found", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]) // PurchaseBillLine rows
      .mockReturnValueOnce([]) // GoodsReceipt posted
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine found
      .mockReturnValueOnce(undefined); // PurchaseOrder not found → continue
    // calculateDueSchedule then update
    spies.update.mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" });

    const result = await run(db, { id: basePurchaseBill.id, matchingMode: "TWO_WAY" }, commandCtx);

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");
  });
});
