import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PaymentScheduleCalculationFailedError,
  PurchaseBillNotBlockedError,
  PurchaseBillNotFoundError,
  ReleaseReasonRequiredError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, mockInventoryQueries } from "../testing/commandTestUtils";
import {
  basePurchaseBillLine,
  basePurchaseOrderLine,
  blockedPurchaseBill,
  orderedPurchaseOrder,
} from "../testing/fixtures";
import { run, type ReleasePurchaseBillInput } from "./releasePurchaseBill";

const baseInventoryExecutionLine = {
  purchaseOrderLineId: basePurchaseOrderLine.id as string,
  inventoryExecutionLineId: "inventory-execution-line-1",
  inventoryExecutionId: "inventory-execution-1",
  quantity: basePurchaseBillLine.quantity as string,
  createdAt: new Date("2026-01-01T00:00:00Z"),
};

function runCommand(
  db: Transaction,
  input: ReleasePurchaseBillInput,
  receiptEvidence = [baseInventoryExecutionLine],
) {
  return run(db, input, commandCtx, mockInventoryQueries({ receiptEvidence }));
}

describe("releasePurchaseBill", () => {
  it("releases a blocked purchase bill with an override reason", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine); // getOrderLine (per bill line)
    // matchingMode is not THREE_WAY → no inventory receipt evidence/planThreeWayMatch queries
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Approved",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...blockedPurchaseBill, status: "MATCHED", id: blockedPurchaseBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await runCommand(db, {
      id: blockedPurchaseBill.id,
      releaseReason: "Approved",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseBill.status).toBe("MATCHED");
  });

  it("preserves original variance evidence when releasing", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine); // getOrderLine (per bill line)
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Approved",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...blockedPurchaseBill, status: "MATCHED", id: blockedPurchaseBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    await runCommand(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" });

    // The PurchaseBill update is the 1st set() call now
    const setCall = spies.set.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(setCall).not.toHaveProperty("comparisonEvidence");
  });

  it("returns error when purchase bill does not exist", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(undefined);
    expectErr(
      await runCommand(db, { id: "missing-bill", releaseReason: "Approved" }),
      PurchaseBillNotFoundError,
    );
  });

  it("returns error when purchase bill is not blocked", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce({ ...blockedPurchaseBill, status: "DRAFT" });
    expectErr(
      await runCommand(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" }),
      PurchaseBillNotBlockedError,
    );
  });

  it("returns error when override reason is missing", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select.mockReturnValueOnce(blockedPurchaseBill);
    expectErr(
      await runCommand(db, { id: blockedPurchaseBill.id, releaseReason: "   " }),
      ReleaseReasonRequiredError,
    );
  });

  it("creates ThreeWayMatchEvent records when releasing a THREE_WAY blocked bill", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const threeWayBlockedBill = {
      ...blockedPurchaseBill,
      matchingMode: "THREE_WAY",
      varianceReason: "RECEIPT_QUANTITY_EXCEEDED",
    };
    const billLine = { ...basePurchaseBillLine, purchaseBillId: threeWayBlockedBill.id };
    spies.select
      .mockReturnValueOnce(threeWayBlockedBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(basePurchaseOrderLine) // getOrderLine (per bill line)
      .mockReturnValueOnce([]); // ThreeWayMatchEvent (existing matches)
    // ThreeWayMatchEvent insert
    spies.insert.mockReturnValueOnce(undefined);
    // UPDATE PurchaseBill (set status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...threeWayBlockedBill,
      status: "MATCHED",
      releaseReason: "Approved override",
    });
    // updatePOBillingStatus
    spies.select
      .mockReturnValueOnce([basePurchaseOrderLine]) // PurchaseOrderLine rows (per order)
      .mockReturnValueOnce([billLine]) // PurchaseBillLine (for order lines)
      .mockReturnValueOnce([
        { ...threeWayBlockedBill, status: "MATCHED", id: threeWayBlockedBill.id },
      ]); // PurchaseBill (status=MATCHED)
    spies.update.mockReturnValueOnce({
      ...orderedPurchaseOrder,
      billingStatus: "PARTIALLY_BILLED",
    }); // update PurchaseOrder

    const result = await runCommand(db, {
      id: threeWayBlockedBill.id,
      releaseReason: "Approved override",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseBill.status).toBe("MATCHED");
    // Verify ThreeWayMatchEvent was inserted
    expect(spies.values).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          purchaseBillLineId: billLine.id,
          inventoryExecutionLineId: baseInventoryExecutionLine.inventoryExecutionLineId,
        }),
      ]),
    );
  });

  it("releases a bill with PO-unlinked lines (null purchaseOrderLineId)", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const unlinkedBillLine = {
      ...basePurchaseBillLine,
      purchaseBillId: blockedPurchaseBill.id,
      purchaseOrderLineId: null,
      billedUnitPrice: null,
    };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([unlinkedBillLine]); // PurchaseBillLine rows
    // No getOrderLine call since purchaseOrderLineId is null
    // UPDATE PurchaseBill
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Override",
    });
    // updatePOBillingStatus — no affected orders

    const result = await runCommand(db, {
      id: blockedPurchaseBill.id,
      releaseReason: "Override",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseBill.status).toBe("MATCHED");
  });

  it("skips order line when getOrderLine returns undefined", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billLine = { ...basePurchaseBillLine, purchaseBillId: blockedPurchaseBill.id };
    spies.select
      .mockReturnValueOnce(blockedPurchaseBill) // PurchaseBill
      .mockReturnValueOnce([billLine]) // PurchaseBillLine rows
      .mockReturnValueOnce(undefined); // getOrderLine returns undefined
    // UPDATE PurchaseBill
    spies.update.mockReturnValueOnce({
      ...blockedPurchaseBill,
      status: "MATCHED",
      releaseReason: "Override",
    });
    // updatePOBillingStatus — no affected orders (orderLine was skipped)

    const result = await runCommand(db, {
      id: blockedPurchaseBill.id,
      releaseReason: "Override",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.purchaseBill.status).toBe("MATCHED");
  });

  it("falls back to empty array when bill paymentTermSnapshotLines is null", async () => {
    const { db, spies } = createMockDb<Transaction>();
    const billWithNullTerms = {
      ...blockedPurchaseBill,
      paymentTermSnapshotLines: null,
    };
    const billLine = { ...basePurchaseBillLine, purchaseBillId: billWithNullTerms.id };
    spies.select
      .mockReturnValueOnce(billWithNullTerms) // PurchaseBill
      .mockReturnValueOnce([billLine]); // PurchaseBillLine rows
    // calculateDueSchedule with empty array will fail
    expectErr(
      await runCommand(db, { id: billWithNullTerms.id, releaseReason: "Override" }),
      PaymentScheduleCalculationFailedError,
    );
  });

  it("returns error when payment schedule calculation fails", async () => {
    const { db, spies } = createMockDb<Transaction>();
    spies.select
      .mockReturnValueOnce({
        ...blockedPurchaseBill,
        paymentTermSnapshotLines: [],
      }) // PurchaseBill
      .mockReturnValueOnce([basePurchaseBillLine]); // PurchaseBillLine rows
    expectErr(
      await runCommand(db, { id: blockedPurchaseBill.id, releaseReason: "Approved" }),
      PaymentScheduleCalculationFailedError,
    );
  });
});
