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 {
  baseGoodsReceiptLines,
  basePurchaseBill,
  basePurchaseBillLines,
  matchedPurchaseBill,
  orderedPurchaseOrder,
  postedGoodsReceipt,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./matchPurchaseBill";

describe("matchPurchaseBill", () => {
  it("matches a valid 2-way purchase bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([]);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_BILLED" })
      .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("matches a valid 3-way purchase bill when posted receipt quantity is sufficient", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([postedGoodsReceipt]);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_BILLED" })
      .mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "THREE_WAY" });

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

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

  it("blocks a bill when 3-way quantity exceeds posted receipt quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partialReceipt = {
      ...postedGoodsReceipt,
      lines: JSON.stringify([{ ...baseGoodsReceiptLines[0], quantity: 3 }]),
    };
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([partialReceipt]);
    spies.update.mockReturnValueOnce({
      ...basePurchaseBill,
      status: "BLOCKED",
      varianceReason: "RECEIPT_QUANTITY_EXCEEDED",
    });

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

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

  it("blocks a bill when price variance exceeds tolerance", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const highVarianceBill = {
      ...basePurchaseBill,
      lines: JSON.stringify([{ ...basePurchaseBillLines[0], billedUnitPrice: 120 }]),
    };
    spies.select
      .mockReturnValueOnce(highVarianceBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([]);
    spies.update.mockReturnValueOnce({
      ...highVarianceBill,
      status: "BLOCKED",
      varianceReason: "PRICE_VARIANCE_EXCEEDED",
    });

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

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

  it("records matching mode on the bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([]);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_BILLED" })
      .mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" });

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

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

  it("increases purchase-order billed quantity when the bill is matched", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([]);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_BILLED" })
      .mockReturnValueOnce({ ...matchedPurchaseBill, matchingMode: "TWO_WAY" });

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

    const setCall = spies.set.mock.calls[0]?.[0] as { lines?: string };
    expect(setCall.lines).toContain('"billedQuantity":4');
  });

  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)
      .mockReturnValueOnce([{ ...orderedPurchaseOrder, status: "CANCELLED" }])
      .mockReturnValueOnce([]);
    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,
      paymentTermSnapshot: JSON.stringify({ lines: [] }),
    };
    spies.select
      .mockReturnValueOnce(invalidScheduleBill)
      .mockReturnValueOnce([orderedPurchaseOrder])
      .mockReturnValueOnce([]);
    expectErr(
      await run(db, { id: invalidScheduleBill.id, matchingMode: "TWO_WAY" }, commandCtx),
      PaymentScheduleCalculationFailedError,
    );
  });
});
