import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  DuplicateSupplierInvoiceNumberError,
  InvalidBilledQuantityError,
  PurchaseBillNotDraftError,
  PurchaseBillNotFoundError,
  PurchaseOrderCancelledError,
  PurchaseOrderLineNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  basePurchaseBill,
  basePurchaseBillLines,
  basePurchaseOrderLine,
  baseUnit,
  blockedPurchaseBill,
  orderedPurchaseOrder,
} from "../testing/fixtures";
import { run } from "./updatePurchaseBill";

describe("updatePurchaseBill", () => {
  it("updates invoice metadata on a draft purchase bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...basePurchaseBill, invoiceNumber: "INV-UPDATED" };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce(undefined); // findPurchaseBillDuplicate
    spies.update.mockReturnValueOnce(updated); // updateTable PurchaseBill

    const result = await run(
      db,
      { id: basePurchaseBill.id, invoiceNumber: "INV-UPDATED" },
      commandCtx,
    );

    expect(expectOk(result).purchaseBill.invoiceNumber).toBe("INV-UPDATED");
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ invoiceNumber: "INV-UPDATED" }),
    );
  });

  it("updates bill lines on a draft purchase bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...basePurchaseBill };
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine bulk fetch
      .mockReturnValueOnce([orderedPurchaseOrder]); // PurchaseOrder bulk fetch
    spies.update.mockReturnValueOnce(updated); // updateTable PurchaseBill
    // delete PurchaseBillLine + insert PurchaseBillLine

    const result = await run(
      db,
      { id: basePurchaseBill.id, lines: [{ ...basePurchaseBillLines[0], quantity: "5" }] },
      commandCtx,
    );

    expectOk(result);
    expect(spies.values).toHaveBeenCalledWith([
      expect.objectContaining({ purchaseBillId: basePurchaseBill.id, quantity: "5" }),
    ]);
  });

  it("returns error when purchase bill does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, { id: "missing-bill" }, commandCtx);

    expectErr(result, PurchaseBillNotFoundError);
  });

  it("returns error when purchase bill is not draft", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(blockedPurchaseBill);

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

    expectErr(result, PurchaseBillNotDraftError);
  });

  it("returns error when updated invoice number is duplicated", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseBill).mockReturnValueOnce({ id: "existing-bill" });

    const result = await run(
      db,
      { id: basePurchaseBill.id, invoiceNumber: "INV-DUPLICATE" },
      commandCtx,
    );

    expectErr(result, DuplicateSupplierInvoiceNumberError);
  });

  it("returns error when updated line data is invalid", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([]); // PurchaseOrderLine bulk fetch (empty)

    const result = await run(
      db,
      {
        id: basePurchaseBill.id,
        lines: [{ ...basePurchaseBillLines[0], purchaseOrderLineId: "missing-line" }],
      },
      commandCtx,
    );

    expectErr(result, PurchaseOrderLineNotFoundError);
  });

  it("returns error when updated billed quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine bulk fetch
      .mockReturnValueOnce([orderedPurchaseOrder]); // PurchaseOrder bulk fetch

    const result = await run(
      db,
      { id: basePurchaseBill.id, lines: [{ ...basePurchaseBillLines[0], quantity: "0" }] },
      commandCtx,
    );

    expectErr(result, InvalidBilledQuantityError);
  });

  it("returns error when a PO-unlinked line has non-positive quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseBill);

    const result = await run(
      db,
      {
        id: basePurchaseBill.id,
        lines: [{ quantity: "0", purchaseOrderLineId: null, unitId: baseUnit.id }],
      },
      commandCtx,
    );

    expectErr(result, InvalidBilledQuantityError);
  });

  it("accepts PO-unlinked lines with valid quantity and uses defaults", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...basePurchaseBill };
    spies.select.mockReturnValueOnce(basePurchaseBill); // PurchaseBill
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      {
        id: basePurchaseBill.id,
        lines: [
          {
            quantity: "5",
            purchaseOrderLineId: null,
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
    );

    expectOk(result);
    const valuesCall = spies.values.mock.calls[0]?.[0] as Record<string, unknown>[];
    expect(valuesCall[0]).toEqual(
      expect.objectContaining({
        purchaseOrderLineId: null,
        billedUnitPrice: null,
        taxCode: null,
        taxableAmount: null,
        taxAmount: null,
      }),
    );
  });

  it("uses PO line unitPrice as default billedUnitPrice when not provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const updated = { ...basePurchaseBill };
    spies.select
      .mockReturnValueOnce(basePurchaseBill)
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine bulk fetch
      .mockReturnValueOnce([orderedPurchaseOrder]); // PurchaseOrder bulk fetch
    spies.update.mockReturnValueOnce(updated);

    const result = await run(
      db,
      {
        id: basePurchaseBill.id,
        lines: [
          {
            purchaseOrderLineId: basePurchaseOrderLine.id,
            quantity: "4",
          },
        ],
      },
      commandCtx,
    );

    expectOk(result);
    const valuesCall = spies.values.mock.calls[0]?.[0] as Record<string, unknown>[];
    expect(valuesCall[0]).toEqual(
      expect.objectContaining({
        billedUnitPrice: basePurchaseOrderLine.unitPrice,
      }),
    );
  });

  it("returns error when updated bill references a cancelled purchase order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(basePurchaseBill) // PurchaseBill
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine bulk fetch
      .mockReturnValueOnce([{ ...orderedPurchaseOrder, orderStatus: "CANCELLED" }]); // PurchaseOrder bulk fetch

    const result = await run(
      db,
      { id: basePurchaseBill.id, lines: basePurchaseBillLines },
      commandCtx,
    );

    expectErr(result, PurchaseOrderCancelledError);
  });
});
