import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotBlockedError,
  PurchaseBillNotFoundError,
  ReleaseReasonRequiredError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseGoodsReceiptLine,
  basePurchaseBillLine,
  basePurchaseOrderLine,
  blockedPurchaseBill,
  orderedPurchaseOrder,
  postedGoodsReceipt,
} from "../testing/fixtures";
import { run } from "./releasePurchaseBill";

describe("releasePurchaseBill", () => {
  it("releases a blocked purchase bill with an override reason", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine); // getOrderLine (per bill line)
    // matchingMode is not THREE_WAY → no GoodsReceipt/planThreeWayMatch queries
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Approved",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...blockedPurchaseBill, status: "MATCHED", id: blockedPurchaseBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(
      db,
      { id: blockedPurchaseBill.id, releaseReason: "Approved" },
      commandCtx,
    );

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

  it("preserves original variance evidence when releasing", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine); // getOrderLine (per bill line)
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Approved",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...blockedPurchaseBill, status: "MATCHED", id: blockedPurchaseBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    await run(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" }, commandCtx);

    // The PurchaseBill update is the 1st set() call now
    const setCall = spies.set.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(setCall).not.toHaveProperty("comparisonEvidence");
  });

  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", releaseReason: "Approved" }, commandCtx),
      PurchaseBillNotFoundError,
    );
  });

  it("returns error when purchase bill is not blocked", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...blockedPurchaseBill, status: "DRAFT" });
    expectErr(
      await run(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" }, commandCtx),
      PurchaseBillNotBlockedError,
    );
  });

  it("returns error when override reason is missing", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(blockedPurchaseBill);
    expectErr(
      await run(db, { id: blockedPurchaseBill.id, releaseReason: "   " }, commandCtx),
      ReleaseReasonRequiredError,
    );
  });

  it("creates ThreeWayMatchEvent records when releasing a THREE_WAY blocked bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const threeWayBlockedBill = {
      ...blockedPurchaseBill,
      matchingMode: "THREE_WAY",
      varianceReason: "RECEIPT_QUANTITY_EXCEEDED",
    };
    const billLine = { ...basePurchaseBillLine, purchaseBillId: threeWayBlockedBill.id };
    const postedReceiptLine = { ...baseGoodsReceiptLine, goodsReceiptId: postedGoodsReceipt.id };
    spies.select
      .mockReturnValueOnce(threeWayBlockedBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine) // getOrderLine (per bill line)
      // THREE_WAY ThreeWayMatchEvent creation:
      .mockReturnValueOnce([postedGoodsReceipt]) // GoodsReceipt posted
      // planThreeWayMatch: GoodsReceiptLine (direct per PO line ID)
      .mockReturnValueOnce([postedReceiptLine]) // GoodsReceiptLine (for orderLine.id)
      .mockReturnValueOnce([]); // ThreeWayMatchEvent (existing matches)
    // ThreeWayMatchEvent insert
    spies.insert.mockReturnValueOnce(undefined);
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...threeWayBlockedBill,
      status: "MATCHED",
      releaseReason: "Approved override",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...threeWayBlockedBill, status: "MATCHED", id: threeWayBlockedBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await run(
      db,
      { id: threeWayBlockedBill.id, releaseReason: "Approved override" },
      commandCtx,
    );

    expect(expectOk(result).purchaseBill.status).toBe("MATCHED");
    // Verify ThreeWayMatchEvent was inserted
    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        purchaseBillLineId: billLine.id,
        goodsReceiptLineId: postedReceiptLine.id,
      }),
    );
  });

  it("releases a bill with PO-unlinked lines (null purchaseOrderLineId)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unlinkedBillLine = {
      ...basePurchaseBillLine,
      purchaseBillId: blockedPurchaseBill.id,
      purchaseOrderLineId: null,
      billedUnitPrice: null,
    };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([unlinkedBillLine]); // PurchaseBillLine rows
    // No getOrderLine call since purchaseOrderLineId is null
    // UPDATE PurchaseBill
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Override",
    });
    // updatePOBillingStatus — no affected orders

    const result = await run(
      db,
      { id: blockedPurchaseBill.id, releaseReason: "Override" },
      commandCtx,
    );

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

  it("skips order line when getOrderLine returns undefined", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(undefined); // getOrderLine returns undefined
    // UPDATE PurchaseBill
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Override",
    });
    // updatePOBillingStatus — no affected orders (orderLine was skipped)

    const result = await run(
      db,
      { id: blockedPurchaseBill.id, releaseReason: "Override" },
      commandCtx,
    );

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

  it("falls back to empty array when bill paymentTermSnapshotLines is null", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billWithNullTerms = {
      ...blockedPurchaseBill,
      paymentTermSnapshotLines: null,
    };
    const billLine = { ...basePurchaseBillLine, purchaseBillId: billWithNullTerms.id };
    spies.select
      .mockReturnValueOnce(billWithNullTerms) // PurchaseBill
      .mockReturnValueOnce([billLine]); // PurchaseBillLine rows
    // calculateDueSchedule with empty array will fail
    expectErr(
      await run(db, { id: billWithNullTerms.id, releaseReason: "Override" }, commandCtx),
      PaymentScheduleCalculationFailedError,
    );
  });

  it("returns error when payment schedule calculation fails", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce({
        ...blockedPurchaseBill,
        paymentTermSnapshotLines: [],
      }) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]); // PurchaseBillLine rows
    expectErr(
      await run(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" }, commandCtx),
      PaymentScheduleCalculationFailedError,
    );
  });
});
