import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  EmptyPurchaseOrderLinesError,
  ItemNotActiveError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  PartnerNotSupplierError,
  PaymentTermInactiveError,
  PaymentTermNotFoundError,
  ReceivingSiteNotFoundError,
  ReceivingSiteRequiredError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import {
  commandCtx,
  expectErr,
  expectOk,
  mockBPQueries,
  mockItemQueries,
} from "../testing/commandTestUtils";
import {
  baseItem,
  basePaymentTerm,
  basePaymentTermLine,
  basePurchaseOrder,
  basePurchaseOrderLines,
  baseReceivingSite,
  baseSupplier,
  baseUnit,
  inactiveSupplier,
} from "../testing/fixtures";
import { run } from "./createPurchaseOrder";

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

describe("createPurchaseOrder", () => {
  it("creates a draft purchase order with valid supplier and lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite) // header receivingSiteId check
      .mockReturnValueOnce(baseReceivingSite); // line receivingSiteId check
    spies.insert.mockReturnValueOnce(basePurchaseOrder);

    const result = await run(
      db,
      { ...baseInput, lines: basePurchaseOrderLines },
      commandCtx,
      bpQ,
      itemQ,
    );

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

  it("returns error when supplier does not exist", async () => {
    const { db } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: undefined });
    const itemQ = mockItemQueries();

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      SupplierNotFoundError,
    );
  });

  it("returns error when supplier is inactive", async () => {
    const { db } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: inactiveSupplier });
    const itemQ = mockItemQueries();

    expectErr(
      await run(
        db,
        { ...baseInput, supplierId: inactiveSupplier.id, lines: basePurchaseOrderLines },
        commandCtx,
        bpQ,
        itemQ,
      ),
      SupplierNotActiveError,
    );
  });

  it("returns error when partner is not a supplier", async () => {
    const { db } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: undefined });
    const itemQ = mockItemQueries();

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      PartnerNotSupplierError,
    );
  });

  it("returns error when no lines are provided", async () => {
    const { db } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries();

    expectErr(
      await run(db, { ...baseInput, lines: [] }, commandCtx, bpQ, itemQ),
      EmptyPurchaseOrderLinesError,
    );
  });

  it("returns error when an item does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: undefined });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite); // header receivingSiteId check

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      ItemNotFoundError,
    );
  });

  it("returns error when an item is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: { ...baseItem, status: "INACTIVE" } });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite);

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      ItemNotActiveError,
    );
  });

  it("returns error when quantity is zero or negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite);

    expectErr(
      await run(
        db,
        { ...baseInput, lines: [{ ...basePurchaseOrderLines[0], quantity: "0" }] },
        commandCtx,
        bpQ,
        itemQ,
      ),
      InvalidQuantityError,
    );
  });

  it("returns error when unit price is negative", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite);

    expectErr(
      await run(
        db,
        { ...baseInput, lines: [{ ...basePurchaseOrderLines[0], unitPrice: "-1" }] },
        commandCtx,
        bpQ,
        itemQ,
      ),
      InvalidUnitPriceError,
    );
  });

  it("returns error when header receiving site does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(undefined); // header receivingSiteId not found

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      ReceivingSiteNotFoundError,
    );
  });

  it("returns error when line receiving site does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite) // header receivingSiteId check
      .mockReturnValueOnce(undefined); // line receivingSiteId not found

    expectErr(
      await run(
        db,
        {
          ...baseInput,
          lines: [{ ...basePurchaseOrderLines[0], receivingSiteId: "missing-site" }],
        },
        commandCtx,
        bpQ,
        itemQ,
      ),
      ReceivingSiteNotFoundError,
    );
  });

  it("creates a purchase order with lines that skip physical receipt when requiresPhysicalReceipt is false", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite); // header receivingSiteId check
    spies.insert.mockReturnValueOnce(basePurchaseOrder);

    const result = await run(
      db,
      {
        ...baseInput,
        lines: [
          {
            itemId: baseItem.id,
            quantity: "10",
            unitPrice: "100",
            unitId: baseUnit.id,
            requiresPhysicalReceipt: false,
          },
        ],
      },
      commandCtx,
      bpQ,
      itemQ,
    );

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

  it("stores line receivingSiteId as null when not provided (header is the default)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select
      .mockReturnValueOnce(basePaymentTerm)
      .mockReturnValueOnce([basePaymentTermLine])
      .mockReturnValueOnce(baseReceivingSite); // header receivingSiteId check
    spies.insert.mockReturnValueOnce(basePurchaseOrder);

    await run(
      db,
      {
        ...baseInput,
        lines: [
          {
            itemId: baseItem.id,
            quantity: "10",
            unitPrice: "100",
            unitId: baseUnit.id,
          },
        ],
      },
      commandCtx,
      bpQ,
      itemQ,
    );

    // Second values() call is the line insert (first is PO header)
    const lineValues = spies.values.mock.calls[1]?.[0] as Record<string, unknown>[];
    expect(lineValues[0]).toEqual(
      expect.objectContaining({
        receivingSiteId: null,
        requiresPhysicalReceipt: true,
      }),
    );
  });

  it("creates a service-only purchase order without header receivingSiteId", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select.mockReturnValueOnce(basePaymentTerm).mockReturnValueOnce([basePaymentTermLine]);
    spies.insert.mockReturnValueOnce({ ...basePurchaseOrder, receivingSiteId: null });

    const result = await run(
      db,
      {
        companyId: basePurchaseOrder.companyId,
        supplierId: baseSupplier.id,
        orderDate: basePurchaseOrder.orderDate,
        paymentTermId: basePaymentTerm.id,
        lines: [
          {
            itemId: baseItem.id,
            quantity: "10",
            unitPrice: "100",
            unitId: baseUnit.id,
            requiresPhysicalReceipt: false,
          },
        ],
      },
      commandCtx,
      bpQ,
      itemQ,
    );

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

  it("returns error when physical line has no receiving site and header has none", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries({ item: baseItem });
    spies.select.mockReturnValueOnce(basePaymentTerm).mockReturnValueOnce([basePaymentTermLine]);

    expectErr(
      await run(
        db,
        {
          companyId: basePurchaseOrder.companyId,
          supplierId: baseSupplier.id,
          orderDate: basePurchaseOrder.orderDate,
          paymentTermId: basePaymentTerm.id,
          lines: [
            {
              itemId: baseItem.id,
              quantity: "10",
              unitPrice: "100",
              unitId: baseUnit.id,
              requiresPhysicalReceipt: true,
              receivingSiteId: null,
            },
          ],
        },
        commandCtx,
        bpQ,
        itemQ,
      ),
      ReceivingSiteRequiredError,
    );
  });

  it("returns error when specified payment term does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries();
    spies.select.mockReturnValueOnce(undefined); // PurchasePaymentTerm not found

    expectErr(
      await run(
        db,
        { ...baseInput, paymentTermId: "missing-payment-term", lines: basePurchaseOrderLines },
        commandCtx,
        bpQ,
        itemQ,
      ),
      PaymentTermNotFoundError,
    );
  });

  it("returns error when specified payment term is inactive", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const bpQ = mockBPQueries({ partner: baseSupplier, partnerRole: { role: "SUPPLIER" } });
    const itemQ = mockItemQueries();
    spies.select.mockReturnValueOnce({ ...basePaymentTerm, status: "INACTIVE" });

    expectErr(
      await run(db, { ...baseInput, lines: basePurchaseOrderLines }, commandCtx, bpQ, itemQ),
      PaymentTermInactiveError,
    );
  });
});
