import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  GoodsReceiptNotDraftError,
  GoodsReceiptNotFoundError,
  InactiveExecutionReferenceError,
  InventoryHandoffFailedError,
  NoOpenReceiptQuantityError,
  OverReceiptToleranceExceededError,
  PurchaseOrderNotReceivableError,
} from "../lib/errors.generated";
import {
  baseGoodsReceipt,
  baseGoodsReceiptLines,
  baseItem,
  basePurchaseOrderLines,
  orderedPurchaseOrder,
  postedGoodsReceipt,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./postGoodsReceipt";

describe("postGoodsReceipt", () => {
  it("posts a valid goods receipt and increases purchase-order received quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: baseGoodsReceipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_RECEIVED" })
      .mockReturnValueOnce({ ...baseGoodsReceipt, status: "POSTED" });

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

    expect(expectOk(result).goodsReceipt.status).toBe("POSTED");
    const orderUpdate = spies.set.mock.calls[0]?.[0] as { lines?: string };
    expect(orderUpdate.lines).toContain('"receivedQuantity":4');
  });

  it("transitions purchase order to partially received when open quantity remains", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: baseGoodsReceipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_RECEIVED" })
      .mockReturnValueOnce({ ...baseGoodsReceipt, status: "POSTED" });

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

    expect(spies.set).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ status: "PARTIALLY_RECEIVED" }),
    );
  });

  it("transitions purchase order to received when all quantity is posted", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receipt = {
      ...baseGoodsReceipt,
      lines: JSON.stringify([{ ...baseGoodsReceiptLines[0], quantity: 10 }]),
    };
    const order = {
      ...orderedPurchaseOrder,
      lines: JSON.stringify([{ ...basePurchaseOrderLines[0], quantity: 10, receivedQuantity: 0 }]),
    };
    spies.select
      .mockReturnValueOnce(receipt)
      .mockReturnValueOnce(order)
      .mockReturnValueOnce({ id: receipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    spies.update
      .mockReturnValueOnce({ ...order, status: "RECEIVED" })
      .mockReturnValueOnce({ ...receipt, status: "POSTED" });

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

    expect(spies.set).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: "RECEIVED" }));
  });

  it("emits inventory handoff once for a stock-item receipt", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: baseGoodsReceipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_RECEIVED" })
      .mockReturnValueOnce({ ...baseGoodsReceipt, status: "POSTED" });

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

    const receiptUpdate = spies.set.mock.calls[1]?.[0] as { inventoryHandoff?: string | null };
    expect(receiptUpdate.inventoryHandoff).toContain(baseGoodsReceipt.id);
  });

  it("skips inventory handoff for a service receipt", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const serviceReceipt = {
      ...baseGoodsReceipt,
      lines: JSON.stringify([{ ...baseGoodsReceiptLines[0], requiresInventoryHandoff: false }]),
    };
    spies.select
      .mockReturnValueOnce(serviceReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: serviceReceipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    spies.update
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "PARTIALLY_RECEIVED" })
      .mockReturnValueOnce({ ...serviceReceipt, status: "POSTED" });

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

    expect(spies.set).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({ inventoryHandoff: null }),
    );
  });

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

  it("returns error when goods receipt is not draft", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(postedGoodsReceipt);
    expectErr(await run(db, { id: postedGoodsReceipt.id }, commandCtx), GoodsReceiptNotDraftError);
  });

  it("returns error when posting exceeds tolerance", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const receipt = {
      ...baseGoodsReceipt,
      lines: JSON.stringify([{ ...baseGoodsReceiptLines[0], quantity: 11 }]),
    };
    const order = {
      ...orderedPurchaseOrder,
      lines: JSON.stringify([{ ...basePurchaseOrderLines[0], quantity: 10, receivedQuantity: 0 }]),
    };
    spies.select
      .mockReturnValueOnce(receipt)
      .mockReturnValueOnce(order)
      .mockReturnValueOnce({ id: receipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    expectErr(await run(db, { id: receipt.id }, commandCtx), OverReceiptToleranceExceededError);
  });

  it("returns error when posting a receipt line with no open quantity remaining", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const order = {
      ...orderedPurchaseOrder,
      lines: JSON.stringify([{ ...basePurchaseOrderLines[0], quantity: 10, receivedQuantity: 10 }]),
    };
    const receipt = {
      ...baseGoodsReceipt,
      lines: JSON.stringify([{ ...baseGoodsReceiptLines[0], quantity: 1 }]),
    };
    spies.select
      .mockReturnValueOnce(receipt)
      .mockReturnValueOnce(order)
      .mockReturnValueOnce({ id: receipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);

    expectErr(await run(db, { id: receipt.id }, commandCtx), NoOpenReceiptQuantityError);
  });

  it("returns error when inventory handoff fails", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: baseGoodsReceipt.supplierId, status: "ACTIVE" })
      .mockReturnValueOnce(baseItem);
    expectErr(
      await run(db, { id: baseGoodsReceipt.id, inventoryHandoffSucceeded: false }, commandCtx),
      InventoryHandoffFailedError,
    );
  });

  it("returns error when purchase order is no longer receivable", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce({ ...orderedPurchaseOrder, status: "CANCELLED" });
    expectErr(
      await run(db, { id: baseGoodsReceipt.id }, commandCtx),
      PurchaseOrderNotReceivableError,
    );
  });

  it("returns error when execution references are inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseGoodsReceipt)
      .mockReturnValueOnce(orderedPurchaseOrder)
      .mockReturnValueOnce({ id: baseGoodsReceipt.supplierId, status: "INACTIVE" });
    expectErr(
      await run(db, { id: baseGoodsReceipt.id }, commandCtx),
      InactiveExecutionReferenceError,
    );
  });
});
