import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  EmptyPurchaseOrderLinesError,
  ItemNotActiveError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  PartnerNotSupplierError,
  ReceivingSiteNotFoundError,
  ReceivingSiteRequiredError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseItem,
  basePurchaseOrder,
  basePurchaseOrderLine,
  basePurchaseOrderLines,
  baseReceivingSite,
  baseSupplier,
  baseSupplierRole,
  baseUnit,
  inactiveSupplier,
} from "../testing/fixtures";
import { mockBPQueries, mockItemQueries, mockOrgQueries } from "../testing/moduleMocks";
import { run } from "./createPurchaseOrder";

const baseHeader = {
  companyId: basePurchaseOrder.companyId,
  supplierId: baseSupplier.id,
  orderDate: basePurchaseOrder.orderDate,
  receivingSiteId: baseReceivingSite.id,
};

describe("createPurchaseOrder", () => {
  it("creates a draft purchase order with valid supplier and lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });
    mock.enqueueResults([basePurchaseOrder], [basePurchaseOrderLine]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { header: baseHeader, lines: basePurchaseOrderLines },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(expectOk(result).purchaseOrder.orderStatus).toBe("DRAFT");
  });

  it("returns error when supplier does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries();
    const itemQ = mockItemQueries();

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: basePurchaseOrderLines },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      SupplierNotFoundError,
    );
  });

  it("returns error when supplier is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: inactiveSupplier });
    const itemQ = mockItemQueries();

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            header: { ...baseHeader, supplierId: inactiveSupplier.id },
            lines: basePurchaseOrderLines,
          },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      SupplierNotActiveError,
    );
  });

  it("returns error when partner is not a supplier", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier });
    const itemQ = mockItemQueries();

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: basePurchaseOrderLines },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      PartnerNotSupplierError,
    );
  });

  it("returns error when no lines are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries();

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: [] },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      EmptyPurchaseOrderLinesError,
    );
  });

  it("returns error when an item does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries();

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: basePurchaseOrderLines },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      ItemNotFoundError,
    );
  });

  it("returns error when an item is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: { ...baseItem, status: "INACTIVE" } });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: basePurchaseOrderLines },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      ItemNotActiveError,
    );
  });

  it("returns error when quantity is zero or negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: [{ ...basePurchaseOrderLines[0], quantity: "0" }] },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      InvalidQuantityError,
    );
  });

  it("returns error when unit price is negative", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: [{ ...basePurchaseOrderLines[0], unitPrice: "-1" }] },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      InvalidUnitPriceError,
    );
  });

  it("returns error when header receiving site does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          { header: baseHeader, lines: basePurchaseOrderLines },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries(),
        ),
      ),
      ReceivingSiteNotFoundError,
    );
  });

  it("returns error when line receiving site does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            header: baseHeader,
            lines: [{ ...basePurchaseOrderLines[0], receivingSiteId: "missing-site" }],
          },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries(),
        ),
      ),
      ReceivingSiteNotFoundError,
    );
  });

  it("creates a purchase order with lines that skip physical receipt when requiresPhysicalReceipt is false", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });
    mock.enqueueResults([basePurchaseOrder], [basePurchaseOrderLine]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: baseHeader,
          lines: [
            {
              itemId: baseItem.id,
              quantity: "10",
              unitPrice: "100",
              unitId: baseUnit.id,
              requiresPhysicalReceipt: false,
            },
          ],
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(expectOk(result).purchaseOrder.orderStatus).toBe("DRAFT");
  });

  it("stores line receivingSiteId as null when not provided (header is the default)", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });
    mock.enqueueResults([basePurchaseOrder], [basePurchaseOrderLine]);

    await mock.withTx((trx) =>
      run(
        trx,
        {
          header: baseHeader,
          lines: [
            {
              itemId: baseItem.id,
              quantity: "10",
              unitPrice: "100",
              unitId: baseUnit.id,
            },
          ],
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    // Second insert is the line insert (first is PO header)
    const lineValues = mock.inserts[1].insertRows();
    expect(lineValues[0]).toEqual(
      expect.objectContaining({
        receivingSiteId: null,
        requiresPhysicalReceipt: true,
      }),
    );
  });

  it("creates a service-only purchase order without header receivingSiteId", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });
    mock.enqueueResults([{ ...basePurchaseOrder, receivingSiteId: null }], [basePurchaseOrderLine]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: {
            companyId: basePurchaseOrder.companyId,
            supplierId: baseSupplier.id,
            orderDate: basePurchaseOrder.orderDate,
          },
          lines: [
            {
              itemId: baseItem.id,
              quantity: "10",
              unitPrice: "100",
              unitId: baseUnit.id,
              requiresPhysicalReceipt: false,
            },
          ],
        },
        commandCtx,
        bpQ,
        itemQ,
        mockOrgQueries({ site: baseReceivingSite }),
      ),
    );

    expect(expectOk(result).purchaseOrder.receivingSiteId).toBeNull();
  });

  it("returns error when physical line has no receiving site and header has none", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: baseSupplierRole });
    const itemQ = mockItemQueries({ item: baseItem });

    expectErr(
      await mock.withTx((trx) =>
        run(
          trx,
          {
            header: {
              companyId: basePurchaseOrder.companyId,
              supplierId: baseSupplier.id,
              orderDate: basePurchaseOrder.orderDate,
            },
            lines: [
              {
                itemId: baseItem.id,
                quantity: "10",
                unitPrice: "100",
                unitId: baseUnit.id,
                requiresPhysicalReceipt: true,
                receivingSiteId: null,
              },
            ],
          },
          commandCtx,
          bpQ,
          itemQ,
          mockOrgQueries({ site: baseReceivingSite }),
        ),
      ),
      ReceivingSiteRequiredError,
    );
  });
});
