import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  CoNotApprovedError,
  CoNotFoundError,
  ModifyQuantityBelowBilledError,
  ModifyQuantityBelowReceivedError,
  PoNotFoundError,
  RemoveLineHasBillsError,
  RemoveLineHasReceiptsError,
  RevisionMismatchError,
} from "../lib/errors.generated";
import {
  approvedChangeOrder,
  basePurchaseChangeOrder,
  orderedPurchaseOrderWithRevision,
  poLineForChangeOrder,
} from "../testing/fixtures";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./applyPurchaseChangeOrder";

describe("applyPurchaseChangeOrder", () => {
  // Helper to set up standard mocks for a successful apply
  function setupApplyMocks(
    spies: ReturnType<typeof createMockDb<Transaction>>["spies"],
    coLines: unknown[],
    opts: {
      poLines?: unknown[];
      grReceipts?: unknown[];
      grLines?: unknown[];
      matchedBills?: unknown[];
      billLines?: unknown[];
      po?: unknown;
    } = {},
  ) {
    const po = opts.po ?? orderedPurchaseOrderWithRevision;
    const poLines = opts.poLines ?? [poLineForChangeOrder];

    spies.select
      .mockReturnValueOnce(approvedChangeOrder) // selectOne CO
      .mockReturnValueOnce(po) // selectOne PO
      .mockReturnValueOnce(coLines) // CO lines
      .mockReturnValueOnce(poLines) // all PO lines
      .mockReturnValueOnce(opts.grReceipts ?? []) // GoodsReceipt POSTED
      .mockReturnValueOnce(opts.grLines ?? []) // GoodsReceiptLine
      .mockReturnValueOnce(opts.matchedBills ?? []) // PurchaseBill MATCHED
      .mockReturnValueOnce(opts.billLines ?? []); // PurchaseBillLine
  }

  it("applies a valid approved change order with MODIFY lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const modifyCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "MODIFY",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: "120",
      unitPrice: "110",
      unitId: null,
      receivingSiteId: null,
    };
    setupApplyMocks(spies, [modifyCoLine]);

    // insert new PO line
    const newPoLine = {
      ...poLineForChangeOrder,
      id: "po-line-new",
      quantity: "120",
      unitPrice: "110",
      lineStatus: "ACTIVE",
      revisionNumber: 2,
      rootLineId: poLineForChangeOrder.id,
    };
    spies.insert.mockReturnValueOnce(newPoLine);
    // update original line to SUPERSEDED
    spies.update.mockReturnValueOnce(undefined);
    // update PO (revision + statuses)
    spies.update.mockReturnValueOnce(undefined);
    // update CO to APPLIED
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expect(expectOk(result).changeOrder.changeOrderStatus).toBe("APPLIED");
  });

  it("applies a valid approved change order with REMOVE lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const removeCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "REMOVE",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: null,
      unitPrice: null,
      unitId: null,
      receivingSiteId: null,
    };
    setupApplyMocks(spies, [removeCoLine]);

    // update original line to SUPERSEDED (no replacement)
    spies.update.mockReturnValueOnce(undefined);
    // update PO
    spies.update.mockReturnValueOnce(undefined);
    // update CO to APPLIED
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expect(expectOk(result).changeOrder.changeOrderStatus).toBe("APPLIED");
  });

  it("applies a valid approved change order with ADD lines", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const addCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "ADD",
      originalPurchaseOrderLineId: null,
      itemId: "item-new",
      quantity: "50",
      unitPrice: "200",
      unitId: "unit-1",
      receivingSiteId: "site-1",
    };
    setupApplyMocks(spies, [addCoLine]);

    // insert new ADD line
    const newPoLine = {
      id: "po-line-new-add",
      purchaseOrderId: orderedPurchaseOrderWithRevision.id,
      itemId: "item-new",
      quantity: "50",
      unitPrice: "200",
      lineStatus: "ACTIVE",
      revisionNumber: 2,
      rootLineId: null,
    };
    spies.insert.mockReturnValueOnce(newPoLine);
    // update PO
    spies.update.mockReturnValueOnce(undefined);
    // update CO to APPLIED
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expect(expectOk(result).changeOrder.changeOrderStatus).toBe("APPLIED");
  });

  it("increments PO currentRevisionNumber after application", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const addCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "ADD",
      originalPurchaseOrderLineId: null,
      itemId: "item-new",
      quantity: "50",
      unitPrice: "200",
      unitId: null,
      receivingSiteId: null,
    };
    setupApplyMocks(spies, [addCoLine]);

    spies.insert.mockReturnValueOnce({
      id: "po-line-new",
      quantity: "50",
      rootLineId: null,
    });
    spies.update.mockReturnValueOnce(undefined); // update PO
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    await run(db, { id: approvedChangeOrder.id }, commandCtx);

    // Check PO update includes incremented revision
    expect(spies.set).toHaveBeenCalledWith(expect.objectContaining({ currentRevisionNumber: 2 }));
  });

  it("recalculates receiptStatus using lineage carry-forward after MODIFY from 100 to 120 with 100 received", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const origLine = {
      ...poLineForChangeOrder,
      quantity: "100",
    };
    const modifyCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "MODIFY",
      originalPurchaseOrderLineId: origLine.id,
      itemId: null,
      quantity: "120",
      unitPrice: "100",
      unitId: null,
      receivingSiteId: null,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: "gr-1",
      purchaseOrderLineId: origLine.id,
      quantity: "100",
    };
    setupApplyMocks(spies, [modifyCoLine], {
      poLines: [origLine],
      grReceipts: [{ id: "gr-1", status: "POSTED" }],
      grLines: [grLine],
    });

    const newPoLine = {
      ...origLine,
      id: "po-line-new",
      quantity: "120",
      lineStatus: "ACTIVE",
      revisionNumber: 2,
      rootLineId: origLine.id,
    };
    spies.insert.mockReturnValueOnce(newPoLine);
    spies.update.mockReturnValueOnce(undefined); // supersede original
    spies.update.mockReturnValueOnce(undefined); // update PO
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    await run(db, { id: approvedChangeOrder.id }, commandCtx);

    // 100 received out of 120 → PARTIALLY_RECEIVED
    expect(spies.set).toHaveBeenCalledWith(
      expect.objectContaining({ receiptStatus: "PARTIALLY_RECEIVED" }),
    );
  });

  it("recalculates receiptStatus as RECEIVED after MODIFY from 100 to 80 with 80 received", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const origLine = {
      ...poLineForChangeOrder,
      quantity: "100",
    };
    const modifyCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "MODIFY",
      originalPurchaseOrderLineId: origLine.id,
      itemId: null,
      quantity: "80",
      unitPrice: "100",
      unitId: null,
      receivingSiteId: null,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: "gr-1",
      purchaseOrderLineId: origLine.id,
      quantity: "80",
    };
    setupApplyMocks(spies, [modifyCoLine], {
      poLines: [origLine],
      grReceipts: [{ id: "gr-1", status: "POSTED" }],
      grLines: [grLine],
    });

    const newPoLine = {
      ...origLine,
      id: "po-line-new",
      quantity: "80",
      lineStatus: "ACTIVE",
      revisionNumber: 2,
      rootLineId: origLine.id,
    };
    spies.insert.mockReturnValueOnce(newPoLine);
    spies.update.mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce(undefined);
    spies.update.mockReturnValueOnce({ ...approvedChangeOrder, changeOrderStatus: "APPLIED" });

    await run(db, { id: approvedChangeOrder.id }, commandCtx);

    // 80 received out of 80 → RECEIVED
    expect(spies.set).toHaveBeenCalledWith(expect.objectContaining({ receiptStatus: "RECEIVED" }));
  });

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

    const result = await run(db, { id: "missing-co" }, commandCtx);

    expectErr(result, CoNotFoundError);
  });

  it("returns error when change order is not in APPROVED status", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(basePurchaseChangeOrder); // DRAFT

    const result = await run(db, { id: basePurchaseChangeOrder.id }, commandCtx);

    expectErr(result, CoNotApprovedError);
  });

  it("returns PoNotFoundError when purchase order does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce(approvedChangeOrder) // CO found
      .mockReturnValueOnce(undefined); // PO not found

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expectErr(result, PoNotFoundError);
  });

  it("returns error when fromRevisionNumber does not match PO currentRevisionNumber", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const staleApprovedCo = {
      ...approvedChangeOrder,
      fromRevisionNumber: 1,
    };
    const poWithHigherRevision = {
      ...orderedPurchaseOrderWithRevision,
      currentRevisionNumber: 3,
    };
    spies.select.mockReturnValueOnce(staleApprovedCo).mockReturnValueOnce(poWithHigherRevision);

    const result = await run(db, { id: staleApprovedCo.id }, commandCtx);

    expectErr(result, RevisionMismatchError);
  });

  it("returns error when MODIFY reduces quantity below received quantity at apply time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const modifyCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "MODIFY",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: "3",
      unitPrice: "100",
      unitId: null,
      receivingSiteId: null,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: "gr-1",
      purchaseOrderLineId: poLineForChangeOrder.id,
      quantity: "5",
    };
    setupApplyMocks(spies, [modifyCoLine], {
      grReceipts: [{ id: "gr-1", status: "POSTED" }],
      grLines: [grLine],
    });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expectErr(result, ModifyQuantityBelowReceivedError);
  });

  it("returns error when MODIFY reduces quantity below billed quantity at apply time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const modifyCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "MODIFY",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: "3",
      unitPrice: "100",
      unitId: null,
      receivingSiteId: null,
    };
    const billLine = {
      id: "bill-line-1",
      purchaseBillId: "bill-1",
      purchaseOrderLineId: poLineForChangeOrder.id,
      quantity: "5",
    };
    setupApplyMocks(spies, [modifyCoLine], {
      matchedBills: [{ id: "bill-1", status: "MATCHED" }],
      billLines: [billLine],
    });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expectErr(result, ModifyQuantityBelowBilledError);
  });

  it("returns error when REMOVE targets a line with posted receipts at apply time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const removeCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "REMOVE",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: null,
      unitPrice: null,
      unitId: null,
      receivingSiteId: null,
    };
    const grLine = {
      id: "gr-line-1",
      goodsReceiptId: "gr-1",
      purchaseOrderLineId: poLineForChangeOrder.id,
      quantity: "2",
    };
    setupApplyMocks(spies, [removeCoLine], {
      grReceipts: [{ id: "gr-1", status: "POSTED" }],
      grLines: [grLine],
    });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expectErr(result, RemoveLineHasReceiptsError);
  });

  it("returns error when REMOVE targets a line with matched bills at apply time", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const removeCoLine = {
      id: "co-line-1",
      purchaseChangeOrderId: approvedChangeOrder.id,
      action: "REMOVE",
      originalPurchaseOrderLineId: poLineForChangeOrder.id,
      itemId: null,
      quantity: null,
      unitPrice: null,
      unitId: null,
      receivingSiteId: null,
    };
    const billLine = {
      id: "bill-line-1",
      purchaseBillId: "bill-1",
      purchaseOrderLineId: poLineForChangeOrder.id,
      quantity: "2",
    };
    setupApplyMocks(spies, [removeCoLine], {
      matchedBills: [{ id: "bill-1", status: "MATCHED" }],
      billLines: [billLine],
    });

    const result = await run(db, { id: approvedChangeOrder.id }, commandCtx);

    expectErr(result, RemoveLineHasBillsError);
  });
});
