import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CompanyScopeMismatchError,
  EmptyReceiptLinesError,
  InvalidReceivedQuantityError,
  NoOpenReceiptQuantityError,
  PurchaseOrderLineNotFoundError,
  PurchaseOrderNotReceivableError,
  CompanyOrSupplierMismatchError,
} from "../lib/errors.generated";
import {
  baseCompany,
  baseGoodsReceipt,
  baseGoodsReceiptLines,
  basePurchaseOrderLine,
  orderedPurchaseOrder,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./createGoodsReceipt";

describe("createGoodsReceipt", () => {
  it("creates a draft goods receipt with lines from a single ordered purchase order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine (by ids) → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder (by ids) → execute (array)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 5. insertInto GoodsReceipt → executeTakeFirst
    spies.insert.mockReturnValueOnce({
      ...baseGoodsReceipt,
    });
    // 6. insertInto GoodsReceiptLine → executeTakeFirst (one per line)
    spies.insert.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: baseGoodsReceiptLines,
      },
      commandCtx,
    );

    expect(expectOk(result).goodsReceipt.status).toBe("DRAFT");
  });

  it("creates a draft goods receipt with lines from multiple purchase orders of the same supplier (consolidated shipment)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const secondPO = {
      ...orderedPurchaseOrder,
      id: "purchase-order-4",
    };
    const secondPOLine = {
      ...basePurchaseOrderLine,
      id: "po-line-2",
      purchaseOrderId: secondPO.id,
    };
    // 1. selectFrom PurchaseOrderLine (by ids) → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine, secondPOLine]);
    // 2. selectFrom PurchaseOrder (by ids) → execute (array)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder, secondPO]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 5. insertInto GoodsReceipt
    spies.insert.mockReturnValueOnce(baseGoodsReceipt);
    // 4-5. insertInto GoodsReceiptLine (two lines)
    spies.insert.mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [
          { ...baseGoodsReceiptLines[0] },
          { ...baseGoodsReceiptLines[0], purchaseOrderLineId: secondPOLine.id },
        ],
      },
      commandCtx,
    );

    expect(expectOk(result).goodsReceipt.status).toBe("DRAFT");
  });

  it("creates a draft goods receipt against a partially received purchase order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const partiallyReceivedOrder = {
      ...orderedPurchaseOrder,
      receiptStatus: "PARTIALLY_RECEIVED",
    };
    // 1. selectFrom PurchaseOrderLine → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder → execute (array)
    spies.select.mockReturnValueOnce([partiallyReceivedOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 5. insertInto GoodsReceipt
    spies.insert.mockReturnValueOnce(baseGoodsReceipt);
    // 4. insertInto GoodsReceiptLine
    spies.insert.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: partiallyReceivedOrder.supplierId,
        lines: [{ ...baseGoodsReceiptLines[0], quantity: "2" }],
      },
      commandCtx,
    );

    expect(expectOk(result).goodsReceipt).toBeDefined();
  });

  it("creates a draft goods receipt with PO-unlinked lines (blind receipt)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // No PO line lookups needed — lines have no purchaseOrderLineId
    // 1. selectFrom PurchaseOrderLine → execute (empty, no PO-linked lines)
    spies.select.mockReturnValueOnce([]);
    // 2. selectFrom PurchaseOrder → execute (empty)
    spies.select.mockReturnValueOnce([]);
    // 3. insertInto GoodsReceipt
    spies.insert.mockReturnValueOnce(baseGoodsReceipt);
    // 4. insertInto GoodsReceiptLine
    spies.insert.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [{ quantity: "4", itemId: "item-1" }],
      },
      commandCtx,
    );

    expect(expectOk(result).goodsReceipt.status).toBe("DRAFT");
  });

  it("creates a draft goods receipt with a mix of PO-linked and PO-unlinked lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine (by ids for PO-linked line only)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder (by ids)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 5. insertInto GoodsReceipt
    spies.insert.mockReturnValueOnce(baseGoodsReceipt);
    // 6-7. insertInto GoodsReceiptLine (two lines)
    spies.insert.mockReturnValueOnce(undefined);
    spies.insert.mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [{ ...baseGoodsReceiptLines[0] }, { quantity: "2", itemId: "item-1" }],
      },
      commandCtx,
    );

    expect(expectOk(result).goodsReceipt.status).toBe("DRAFT");
  });

  it("returns error when no receipt lines are provided", async () => {
    const { db } = createMockDb<Transaction>();

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [],
      },
      commandCtx,
    );

    expectErr(result, EmptyReceiptLinesError);
  });

  it("returns error when a purchase-order line does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine → execute (empty, line not found)
    spies.select.mockReturnValueOnce([]);
    // 2. selectFrom PurchaseOrder → execute (empty)
    spies.select.mockReturnValueOnce([]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [{ ...baseGoodsReceiptLines[0], purchaseOrderLineId: "missing-line" }],
      },
      commandCtx,
    );

    expectErr(result, PurchaseOrderLineNotFoundError);
  });

  it("returns error when a referenced purchase order is cancelled or closed", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const cancelledPO = { ...orderedPurchaseOrder, orderStatus: "CANCELLED" };
    // 1. selectFrom PurchaseOrderLine → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder → execute (array)
    spies.select.mockReturnValueOnce([cancelledPO]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: baseGoodsReceiptLines,
      },
      commandCtx,
    );

    expectErr(result, PurchaseOrderNotReceivableError);
  });

  it("returns error when a receipt line references a purchase order with a different company", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder → execute (array)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);

    const result = await run(
      db,
      {
        companyId: "company-2",
        supplierId: orderedPurchaseOrder.supplierId,
        lines: baseGoodsReceiptLines,
      },
      commandCtx,
    );

    expectErr(result, CompanyScopeMismatchError);
  });

  it("returns error when a receipt line references a purchase order with a different supplier", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder → execute (array)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: "different-supplier",
        lines: baseGoodsReceiptLines,
      },
      commandCtx,
    );

    expectErr(result, CompanyOrSupplierMismatchError);
  });

  it("returns error when received quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    // 1. selectFrom PurchaseOrderLine → execute (array)
    spies.select.mockReturnValueOnce([basePurchaseOrderLine]);
    // 2. selectFrom PurchaseOrder → execute (array)
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. selectFrom GoodsReceiptLine (existing receipts) → execute (array)
    spies.select.mockReturnValueOnce([]);
    // 4. selectFrom GoodsReceipt (POSTED) → execute (array)
    spies.select.mockReturnValueOnce([]);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [{ ...baseGoodsReceiptLines[0], quantity: "0" }],
      },
      commandCtx,
    );

    expectErr(result, InvalidReceivedQuantityError);
  });

  it("returns error when a receipt line has no open quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const fullyReceivedLine = { ...basePurchaseOrderLine, quantity: 10 };
    // 1. PurchaseOrderLine
    spies.select.mockReturnValueOnce([fullyReceivedLine]);
    // 2. PurchaseOrder
    spies.select.mockReturnValueOnce([orderedPurchaseOrder]);
    // 3. GoodsReceiptLine — existing receipt covers full quantity
    spies.select.mockReturnValueOnce([
      {
        goodsReceiptId: "posted-gr-1",
        purchaseOrderLineId: basePurchaseOrderLine.id,
        quantity: 10,
      },
    ]);
    // 4. GoodsReceipt (POSTED)
    spies.select.mockReturnValueOnce([{ id: "posted-gr-1", status: "POSTED" }]);

    const result = await run(
      db,
      {
        companyId: baseCompany.id,
        supplierId: orderedPurchaseOrder.supplierId,
        lines: [{ ...baseGoodsReceiptLines[0], quantity: "1" }],
      },
      commandCtx,
    );

    expectErr(result, NoOpenReceiptQuantityError);
  });
});
