import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PartnerNotSupplierError,
  PaymentTermInactiveError,
  PaymentTermNotFoundError,
  PurchaseRequisitionAlreadyConvertedError,
  PurchaseRequisitionNotApprovedError,
  PurchaseRequisitionNotFoundError,
  ReceivingSiteNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
  SupplierRequiredError,
} from "../lib/errors.generated";
import {
  commandCtx,
  expectErr,
  expectOk,
  mockBPQueries,
  mockItemQueries,
} from "../testing/commandTestUtils";
import {
  approvedPurchaseRequisition,
  basePurchaseRequisitionLine,
  baseItem,
  basePaymentTerm,
  basePaymentTermLine,
  basePriceRule,
  baseReceivingSite,
  baseSupplier,
  baseSupplierRole,
  inactivePaymentTerm,
  convertedPurchaseRequisition,
  inactiveSupplier,
  submittedPurchaseRequisition,
} from "../testing/fixtures";
import { run } from "./convertPurchaseRequisitionToPurchaseOrder";

const lineWithSuggestedSupplier = {
  ...basePurchaseRequisitionLine,
  purchaseRequisitionId: approvedPurchaseRequisition.id,
  suggestedSupplierId: baseSupplier.id,
};

const lineWithoutSuggestedSupplier = {
  ...basePurchaseRequisitionLine,
  purchaseRequisitionId: approvedPurchaseRequisition.id,
  suggestedSupplierId: null,
};

const createdPurchaseOrder = {
  id: "generated-po-1",
  companyId: approvedPurchaseRequisition.companyId,
  supplierId: baseSupplier.id,
  orderStatus: "DRAFT",
  receiptStatus: null,
  billingStatus: null,
};

const createdPurchaseOrderLine = {
  id: "generated-po-line-1",
  purchaseOrderId: createdPurchaseOrder.id,
  itemId: baseItem.id,
  quantity: basePurchaseRequisitionLine.quantity,
  unitPrice: basePriceRule.unitPrice,
};

const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
const itemQ = mockItemQueries({ item: baseItem });

describe("convertPurchaseRequisitionToPurchaseOrder", () => {
  it("converts an approved requisition into a draft purchase order", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce([lineWithSuggestedSupplier]) // selectMany PurchaseRequisitionLine
      .mockReturnValueOnce(basePaymentTerm) // selectOne PurchasePaymentTerm
      .mockReturnValueOnce([basePaymentTermLine]) // execute PurchasePaymentTermLine
      .mockReturnValueOnce(basePriceRule); // PurchasePriceRule lookup
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder) // insertInto PurchaseOrder
      .mockReturnValueOnce(createdPurchaseOrderLine); // insertInto PurchaseOrderLine
    const converted = { ...approvedPurchaseRequisition, status: "CONVERTED" };
    spies.update.mockReturnValueOnce(converted);

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    const value = expectOk(result);
    expect(value.purchaseOrder.orderStatus).toBe("DRAFT");
    expect(value.purchaseRequisition.status).toBe("CONVERTED");
  });

  it("sets requisition status to CONVERTED after successful conversion", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    const converted = { ...approvedPurchaseRequisition, status: "CONVERTED" };
    spies.update.mockReturnValueOnce(converted);

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.set).toHaveBeenCalledWith(expect.objectContaining({ status: "CONVERTED" }));
  });

  it("uses suggested supplier from requisition when available", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    const value = expectOk(result);
    expect(value.purchaseOrder.supplierId).toBe(baseSupplier.id);
  });

  it("requires supplier to be specified when no suggested supplier exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithoutSuggestedSupplier]);

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries(),
      itemQ,
    );

    expectErr(result, SupplierRequiredError);
  });

  it("records line allocation in PurchaseRequisitionLineAllocation table", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        purchaseRequisitionId: approvedPurchaseRequisition.id,
        purchaseRequisitionLineId: lineWithSuggestedSupplier.id,
        purchaseOrderId: createdPurchaseOrder.id,
        purchaseOrderLineId: createdPurchaseOrderLine.id,
        allocatedQuantity: lineWithSuggestedSupplier.quantity,
      }),
    );
  });

  it("preserves requisition line quantities on generated PO lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        quantity: lineWithSuggestedSupplier.quantity,
      }),
    );
  });

  it("preserves requisition line items on generated PO lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        itemId: lineWithSuggestedSupplier.itemId,
      }),
    );
  });

  it("resolves supplier pricing rules as default unit prices on PO lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite) // selectOne Site
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        unitPrice: basePriceRule.unitPrice,
      }),
    );
  });

  it("uses specified payment term on generated PO", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    // First .values() call is for the PO insert
    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      }),
    );
  });

  it("associates generated PO with the same company as the source requisition", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        companyId: approvedPurchaseRequisition.companyId,
      }),
    );
  });

  it("does not modify source requisition line data", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const originalLine = { ...lineWithSuggestedSupplier };
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    // Verify the source line object was not mutated
    expect(lineWithSuggestedSupplier.quantity).toBe(originalLine.quantity);
    expect(lineWithSuggestedSupplier.itemId).toBe(originalLine.itemId);
    expect(lineWithSuggestedSupplier.estimatedUnitPrice).toBe(originalLine.estimatedUnitPrice);
  });

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

    const result = await run(
      db,
      {
        id: "missing-requisition",
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries(),
      itemQ,
    );

    expectErr(result, PurchaseRequisitionNotFoundError);
  });

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

    const result = await run(
      db,
      {
        id: submittedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries(),
      itemQ,
    );

    expectErr(result, PurchaseRequisitionNotApprovedError);
  });

  it("returns error when purchase requisition is already converted", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(convertedPurchaseRequisition);

    const result = await run(
      db,
      {
        id: convertedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries(),
      itemQ,
    );

    expectErr(result, PurchaseRequisitionAlreadyConvertedError);
  });

  it("returns error when no supplier is specified and no suggested supplier exists", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithoutSuggestedSupplier]);

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries(),
      itemQ,
    );

    expectErr(result, SupplierRequiredError);
  });

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

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        supplierId: "missing-supplier",
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries({ partner: undefined }),
      itemQ,
    );

    expectErr(result, SupplierNotFoundError);
  });

  it("returns error when supplier is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier]);

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        supplierId: inactiveSupplier.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries({ partner: inactiveSupplier }),
      itemQ,
    );

    expectErr(result, SupplierNotActiveError);
  });

  it("returns error when supplier is not classified as a supplier", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier]);

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        supplierId: baseSupplier.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      mockBPQueries({ partner: baseSupplier, partnerRole: undefined }),
      itemQ,
    );

    expectErr(result, PartnerNotSupplierError);
  });

  it("falls back to first line suggestedSupplierId when no input supplierId is provided", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier]) // line has suggestedSupplierId
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite) // selectOne Site
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    // No supplierId in input - should fall back to first line's suggestedSupplierId
    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(expectOk(result).purchaseOrder.supplierId).toBe(baseSupplier.id);
  });

  it("converts requisition line without price rule using estimatedUnitPrice", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const lineWithoutPriceRule = {
      ...lineWithSuggestedSupplier,
      estimatedUnitPrice: "25",
    };
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithoutPriceRule])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite) // selectOne Site
      .mockReturnValueOnce(undefined); // No price rule found
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expectOk(result);
    // unitPrice should fall back to estimatedUnitPrice
    expect(spies.values).toHaveBeenCalledWith(expect.objectContaining({ unitPrice: "25" }));
  });

  it("returns error when specified receiving site does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce([lineWithSuggestedSupplier]) // selectMany PurchaseRequisitionLine
      .mockReturnValueOnce(basePaymentTerm) // selectOne PurchasePaymentTerm
      .mockReturnValueOnce([]) // execute PurchasePaymentTermLine
      .mockReturnValueOnce(undefined); // selectOne Site not found

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: "missing-site",
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expectErr(result, ReceivingSiteNotFoundError);
  });

  it("returns error when specified payment term does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce([lineWithSuggestedSupplier]) // selectMany PurchaseRequisitionLine
      .mockReturnValueOnce(undefined); // selectOne PurchasePaymentTerm not found

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: "missing-payment-term",
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expectErr(result, PaymentTermNotFoundError);
  });

  it("returns error when specified payment term is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition) // selectOne PurchaseRequisition
      .mockReturnValueOnce([lineWithSuggestedSupplier]) // selectMany PurchaseRequisitionLine
      .mockReturnValueOnce(inactivePaymentTerm); // selectOne PurchasePaymentTerm (inactive)

    const result = await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: inactivePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expectErr(result, PaymentTermInactiveError);
  });

  it("snapshots item name and SKU onto PO lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedPurchaseRequisition)
      .mockReturnValueOnce([lineWithSuggestedSupplier])
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(basePriceRule);
    spies.insert
      .mockReturnValueOnce(createdPurchaseOrder)
      .mockReturnValueOnce(createdPurchaseOrderLine);
    spies.update.mockReturnValueOnce({ ...approvedPurchaseRequisition, status: "CONVERTED" });

    await run(
      db,
      {
        id: approvedPurchaseRequisition.id,
        receivingSiteId: baseReceivingSite.id,
        paymentTermId: basePaymentTerm.id,
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    expect(spies.values).toHaveBeenCalledWith(
      expect.objectContaining({
        itemSnapshotName: baseItem.name,
        itemSnapshotSku: baseItem.sku,
      }),
    );
  });
});
