import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  PartnerNotSupplierError,
  PurchaseRequisitionAlreadyConvertedError,
  PurchaseRequisitionNotApprovedError,
  PurchaseRequisitionNotFoundError,
  ReceivingSiteNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
  SupplierRequiredError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  approvedPurchaseRequisition,
  basePurchaseRequisitionLine,
  baseItem,
  basePriceRule,
  baseReceivingSite,
  baseSupplier,
  baseSupplierRole,
  convertedPurchaseRequisition,
  inactiveSupplier,
  submittedPurchaseRequisition,
} from "../testing/fixtures";
import { mockBPQueries, mockItemQueries, mockOrgQueries } from "../testing/moduleMocks";
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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition], // selectOne PurchaseRequisition
      [lineWithSuggestedSupplier], // selectMany PurchaseRequisitionLine
      [basePriceRule], // PurchasePriceRule lookup
      [createdPurchaseOrder], // insertInto PurchaseOrder
      [createdPurchaseOrderLine], // insertInto PurchaseOrderLine
      [], // insertInto PurchaseRequisitionLineAllocation
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }], // update PurchaseRequisition
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({ status: "CONVERTED" }),
    );
  });

  it("uses suggested supplier from requisition when available", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

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

  it("requires supplier to be specified when no suggested supplier exists", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([approvedPurchaseRequisition], [lineWithoutSuggestedSupplier]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, SupplierRequiredError);
  });

  it("records line allocation in PurchaseRequisitionLineAllocation table", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[2].insertValues()).toEqual(
      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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        quantity: lineWithSuggestedSupplier.quantity,
      }),
    );
  });

  it("preserves requisition line items on generated PO lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        itemId: lineWithSuggestedSupplier.itemId,
      }),
    );
  });

  it("resolves supplier pricing rules as default unit prices on PO lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        unitPrice: basePriceRule.unitPrice,
      }),
    );
  });

  it("associates generated PO with the same company as the source requisition", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({
        companyId: approvedPurchaseRequisition.companyId,
      }),
    );
  });

  it("does not modify source requisition line data", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const originalLine = { ...lineWithSuggestedSupplier };
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    // 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 mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: "missing-requisition",
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, PurchaseRequisitionNotFoundError);
  });

  it("returns error when purchase requisition is not approved", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([submittedPurchaseRequisition]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: submittedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, PurchaseRequisitionNotApprovedError);
  });

  it("returns error when purchase requisition is already converted", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([convertedPurchaseRequisition]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: convertedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, PurchaseRequisitionAlreadyConvertedError);
  });

  it("returns error when no supplier is specified and no suggested supplier exists", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([approvedPurchaseRequisition], [lineWithoutSuggestedSupplier]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, SupplierRequiredError);
  });

  it("returns error when supplier does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([approvedPurchaseRequisition], [lineWithSuggestedSupplier]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          supplierId: "missing-supplier",
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries(),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, SupplierNotFoundError);
  });

  it("returns error when supplier is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([approvedPurchaseRequisition], [lineWithSuggestedSupplier]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          supplierId: inactiveSupplier.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries({ partner: inactiveSupplier }),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, SupplierNotActiveError);
  });

  it("returns error when supplier is not classified as a supplier", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([approvedPurchaseRequisition], [lineWithSuggestedSupplier]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          supplierId: baseSupplier.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        mockBPQueries({ partner: baseSupplier }),
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectErr(result, PartnerNotSupplierError);
  });

  it("falls back to first line suggestedSupplierId when no input supplierId is provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier], // line has suggestedSupplierId
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    // No supplierId in input - should fall back to first line's suggestedSupplierId
    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

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

  it("converts requisition line without price rule using estimatedUnitPrice", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const lineWithoutPriceRule = {
      ...lineWithSuggestedSupplier,
      estimatedUnitPrice: "25",
    };
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithoutPriceRule],
      [], // No price rule found
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expectOk(result);
    // unitPrice should fall back to estimatedUnitPrice
    expect(mock.inserts[1].insertValues()).toEqual(expect.objectContaining({ unitPrice: "25" }));
  });

  it("returns error when specified receiving site does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition], // selectOne PurchaseRequisition
      [lineWithSuggestedSupplier], // selectMany PurchaseRequisitionLine
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: "missing-site",
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries(),
      ),
    );

    expectErr(result, ReceivingSiteNotFoundError);
  });

  it("snapshots item name and SKU onto PO lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [approvedPurchaseRequisition],
      [lineWithSuggestedSupplier],
      [basePriceRule],
      [createdPurchaseOrder],
      [createdPurchaseOrderLine],
      [],
      [{ ...approvedPurchaseRequisition, status: "CONVERTED" }],
    );

    await mock.withTx((trx) =>
      run(
        trx,
        {
          id: approvedPurchaseRequisition.id,
          receivingSiteId: baseReceivingSite.id,
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(mock.inserts[1].insertValues()).toEqual(
      expect.objectContaining({
        itemSnapshotName: baseItem.name,
        itemSnapshotSku: baseItem.sku,
      }),
    );
  });
});
