import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  DuplicateSupplierInvoiceNumberError,
  EmptyBillLinesError,
  InvalidBilledQuantityError,
  PaymentTermSnapshotMissingError,
  PurchaseOrderCancelledError,
  PurchaseOrderLineNotFoundError,
  SupplierNotFoundError,
  SupplierTaxIdentificationMissingError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk, mockBPQueries } from "../testing/commandTestUtils";
import {
  basePurchaseBill,
  basePurchaseBillLines,
  basePurchaseOrderLine,
  baseSupplier,
  baseUnit,
  orderedPurchaseOrder,
} from "../testing/fixtures";
import { run } from "./createPurchaseBill";

const bpQ = mockBPQueries();

describe("createPurchaseBill", () => {
  it("creates a draft purchase bill with valid line references", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdBill = { ...basePurchaseBill, id: "purchase-bill-new" };
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder
    spies.insert.mockReturnValueOnce(createdBill); // PurchaseBill insert
    // PurchaseBillLine insert returns are not checked

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-NEW",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expect(expectOk(result).purchaseBill.status).toBe("DRAFT");
    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({ invoiceNumber: "INV-NEW", status: "DRAFT" }),
    );
  });

  it("creates a draft purchase bill with PO-unlinked lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdBill = { ...basePurchaseBill, id: "purchase-bill-unlinked" };
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined); // findPurchaseBillDuplicate
    // No PO line lookups — line has no purchaseOrderLineId
    spies.insert.mockReturnValueOnce(createdBill); // PurchaseBill insert

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-UNLINKED",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [
          {
            purchaseOrderLineId: null,
            quantity: "4",
            billedUnitPrice: "100",
            unitId: baseUnit.id,
            taxCode: "VAT",
            taxableAmount: "400",
            taxAmount: "40",
          },
        ],
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

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

  it("snapshots payment-term data at create time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder
    spies.insert.mockReturnValueOnce(basePurchaseBill);

    await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-SNAPSHOT",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        paymentTermSnapshotLines: orderedPurchaseOrder.paymentTermSnapshotLines,
      }),
    );
  });

  it("snapshots supplier tax identification at create time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder
    spies.insert.mockReturnValueOnce({
      ...basePurchaseBill,
      supplierTaxIdentification: "TAX-DEFAULT",
    });

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-TAX",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
      },
      commandCtx,
      mockBPQueries({ identifications: [{ value: "TAX-DEFAULT" }] }),
    );

    expect(expectOk(result).purchaseBill.supplierTaxIdentification).toBe("TAX-DEFAULT");
    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({ supplierTaxIdentification: "TAX-DEFAULT" }),
    );
  });

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

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: "missing-supplier",
        invoiceNumber: "INV-001",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, SupplierNotFoundError);
  });

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

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: basePurchaseBill.invoiceNumber,
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, DuplicateSupplierInvoiceNumberError);
  });

  it("returns error when no bill lines are provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(baseSupplier).mockReturnValueOnce(undefined);

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-EMPTY",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, EmptyBillLinesError);
  });

  it("returns error when a purchase-order line does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(undefined); // PurchaseOrderLine not found

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-MISSING-LINE",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [{ ...basePurchaseBillLines[0], purchaseOrderLineId: "missing-line" }],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, PurchaseOrderLineNotFoundError);
  });

  it("returns error when billed quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine); // PurchaseOrderLine

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-BAD-QTY",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [{ ...basePurchaseBillLines[0], quantity: "0" }],
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, InvalidBilledQuantityError);
  });

  it("returns error when referenced purchase order is cancelled", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const cancelledOrder = { ...orderedPurchaseOrder, orderStatus: "CANCELLED" };
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(cancelledOrder); // PurchaseOrder (cancelled)

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-CANCELLED",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, PurchaseOrderCancelledError);
  });

  it("returns error when no payment-term snapshot is available", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce({
        ...orderedPurchaseOrder,
        paymentTermSnapshotLines: null,
      }); // PurchaseOrder

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-NO-TERM",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, PaymentTermSnapshotMissingError);
  });

  it("returns error when supplier tax identification cannot be snapshotted", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-NO-TAX",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
      },
      commandCtx,
      mockBPQueries({ identifications: [] }),
    );

    expectErr(result, SupplierTaxIdentificationMissingError);
  });

  it("returns error when PO-unlinked bill line has zero or negative quantity", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined); // findPurchaseBillDuplicate

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-BAD-UNLINKED",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [{ purchaseOrderLineId: null, quantity: "0", unitId: baseUnit.id }],
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, InvalidBilledQuantityError);
  });

  it("returns error when PO is not found for a PO-linked bill line", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine found
      .mockReturnValueOnce(undefined); // PurchaseOrder not found

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-NO-PO",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: basePurchaseBillLines,
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expectErr(result, PurchaseOrderLineNotFoundError);
  });

  it("uses PO line unitPrice as default billedUnitPrice when not provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdBill = { ...basePurchaseBill, id: "purchase-bill-defaults" };
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined) // findPurchaseBillDuplicate
      .mockReturnValueOnce(basePurchaseOrderLine) // PurchaseOrderLine
      .mockReturnValueOnce(orderedPurchaseOrder); // PurchaseOrder
    spies.insert.mockReturnValueOnce(createdBill); // PurchaseBill insert

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-DEFAULTS",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [
          {
            purchaseOrderLineId: basePurchaseOrderLine.id,
            quantity: "4",
            // Not providing billedUnitPrice, taxCode, taxableAmount, taxAmount
          },
        ],
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expectOk(result);
    // Verify PurchaseBillLine values used PO line default unitPrice
    expect(spies.values).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          billedUnitPrice: basePurchaseOrderLine.unitPrice,
          taxCode: null,
          taxableAmount: null,
          taxAmount: null,
        }),
      ]),
    );
  });

  it("creates PO-unlinked bill line with provided optional fields", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const createdBill = { ...basePurchaseBill, id: "purchase-bill-unlinked-opts" };
    spies.select
      .mockReturnValueOnce(baseSupplier) // BusinessPartner
      .mockReturnValueOnce(undefined); // findPurchaseBillDuplicate
    spies.insert.mockReturnValueOnce(createdBill);

    const result = await run(
      db,
      {
        companyId: basePurchaseBill.companyId,
        supplierId: baseSupplier.id,
        invoiceNumber: "INV-UNLINKED-OPTS",
        invoiceDate: basePurchaseBill.invoiceDate,
        lines: [
          {
            purchaseOrderLineId: null,
            quantity: "5",
            unitId: baseUnit.id,
            // No billedUnitPrice, taxCode, taxableAmount, taxAmount
          },
        ],
        supplierTaxIdentification: "TAX-SNAPSHOT",
      },
      commandCtx,
      bpQ,
    );

    expectOk(result);
    expect(spies.values).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          purchaseOrderLineId: null,
          billedUnitPrice: null,
          taxCode: null,
          taxableAmount: null,
          taxAmount: null,
        }),
      ]),
    );
  });
});
