import { err, InsufficientPermissionError } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  ApPurchaseOrderBillingSyncFailedError,
  ApDocumentNotFoundError,
  ApDueScheduleInvalidError,
  ApInvalidDocumentStatusError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccountPayableDocument,
  baseAccountPayableDistributionLine,
  baseAccountPayableDocumentLine,
  baseAccountPayableDueScheduleLine,
  basePurchaseOrderLineForMatching,
  basePurchaseReceiptQuantityForMatching,
  postedAccountPayableDocument,
  registeredAccountPayableDocument,
} from "../testing/fixtures";
import {
  mockInventoryQueries,
  mockPurchaseCommands,
  mockPurchaseQueries,
} from "../testing/moduleMocks";
import { run } from "./registerAccountPayableDocument";

function runRegister(
  mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>,
  input: Parameters<typeof run>[1],
  purchaseQueries = mockPurchaseQueries({
    purchaseOrderLines: [basePurchaseOrderLineForMatching],
  }),
  inventoryQueries = mockInventoryQueries({
    receiptQuantities: [basePurchaseReceiptQuantityForMatching],
  }),
  purchaseCommands = mockPurchaseCommands(),
) {
  return mock.withTx((trx) =>
    run(trx, input, commandCtx, purchaseQueries, inventoryQueries, purchaseCommands),
  );
}

describe("registerAccountPayableDocument", () => {
  it("registers DRAFT AP document without creating a journal entry", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDueScheduleLine],
      [], // supplier invoice tolerance
      [], // already-invoiced quantities
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
    );

    const purchaseCommands = mockPurchaseCommands();
    const result = await runRegister(
      mock,
      { id: baseAccountPayableDocument.id },
      mockPurchaseQueries({ purchaseOrderLines: [basePurchaseOrderLineForMatching] }),
      mockInventoryQueries({ receiptQuantities: [basePurchaseReceiptQuantityForMatching] }),
      purchaseCommands,
    );

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("REGISTERED");
    expect(mock.inserts).toHaveLength(0);
    expect(purchaseCommands.recalculatePurchaseOrderBillingStatus.mock.lastCall?.[1]).toEqual({
      lineBillings: [{ purchaseOrderLineId: "purchase-order-line-1", billedQuantity: "10" }],
    });
  });

  it("registers but places a system hold when the invoiced unit differs from the purchase order", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDueScheduleLine],
      [], // supplier invoice tolerance
      [], // already-invoiced quantities
      [], // hold insert
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
    );

    const result = await runRegister(
      mock,
      { id: baseAccountPayableDocument.id },
      mockPurchaseQueries({
        purchaseOrderLines: [
          { ...basePurchaseOrderLineForMatching, unitId: "unit-2", requiresPhysicalReceipt: false },
        ],
      }),
    );

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("REGISTERED");
    expect(mock.inserts).toHaveLength(1);
  });

  it("registers but places a system hold when the invoiced unit price differs from the purchase order", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDueScheduleLine],
      [], // supplier invoice tolerance
      [], // already-invoiced quantities
      [], // hold insert
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
    );

    const result = await runRegister(
      mock,
      { id: baseAccountPayableDocument.id },
      mockPurchaseQueries({
        purchaseOrderLines: [{ ...basePurchaseOrderLineForMatching, unitPrice: "90" }],
      }),
    );

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("REGISTERED");
    expect(mock.inserts).toHaveLength(1);
  });

  it("registers without placing a system hold when supplier tolerance covers the variance", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDueScheduleLine],
      [
        {
          id: "invoice-tolerance-1",
          companyId: baseAccountPayableDocument.companyId,
          supplierId: baseAccountPayableDocument.businessPartnerId,
          quantityAbsoluteTolerance: null,
          quantityPercentageTolerance: null,
          unitPriceAbsoluteTolerance: null,
          unitPricePercentageTolerance: "0.12",
          createdAt: new Date("2024-01-01T00:00:00.000Z"),
          updatedAt: null,
        },
      ],
      [], // already-invoiced quantities
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
    );

    const result = await runRegister(
      mock,
      { id: baseAccountPayableDocument.id },
      mockPurchaseQueries({
        purchaseOrderLines: [{ ...basePurchaseOrderLineForMatching, unitPrice: "90" }],
      }),
    );

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("REGISTERED");
    expect(mock.inserts).toHaveLength(0);
  });

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

    const result = await runRegister(mock, { id: "missing" });

    expectErr(result, ApDocumentNotFoundError);
  });

  it("returns error when AP document is not DRAFT", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedAccountPayableDocument]);

    const result = await runRegister(mock, { id: postedAccountPayableDocument.id });

    expectErr(result, ApInvalidDocumentStatusError);
  });

  it("returns error when AP document has no lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAccountPayableDocument], []);

    const result = await runRegister(mock, { id: baseAccountPayableDocument.id });

    expectErr(result, ApMinimumLinesNotMetError);
  });

  it("returns error when line total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [{ ...baseAccountPayableDocumentLine, grossAmount: "900" }],
    );

    const result = await runRegister(mock, { id: baseAccountPayableDocument.id });

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when distribution total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [{ ...baseAccountPayableDistributionLine, amount: "900" }],
    );

    const result = await runRegister(mock, { id: baseAccountPayableDocument.id });

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when AP document has no due schedule", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [],
    );

    const result = await runRegister(mock, { id: baseAccountPayableDocument.id });

    expectErr(result, ApDueScheduleInvalidError);
  });

  it("returns error when due schedule total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [{ ...baseAccountPayableDueScheduleLine, amount: "900" }],
    );

    const result = await runRegister(mock, { id: baseAccountPayableDocument.id });

    expectErr(result, ApDueScheduleInvalidError);
  });

  it("returns error when purchase-order billing sync fails", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDueScheduleLine],
      [], // supplier invoice tolerance
      [], // already-invoiced quantities
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
    );

    const purchaseCommands = mockPurchaseCommands();
    purchaseCommands.recalculatePurchaseOrderBillingStatus.mockResolvedValueOnce(
      err(
        new InsufficientPermissionError(
          commandCtx.actorId,
          "purchase:purchaseOrder:recalculatePurchaseOrderBillingStatus",
        ),
      ),
    );

    const result = await runRegister(
      mock,
      { id: baseAccountPayableDocument.id },
      mockPurchaseQueries({ purchaseOrderLines: [basePurchaseOrderLineForMatching] }),
      mockInventoryQueries({ receiptQuantities: [basePurchaseReceiptQuantityForMatching] }),
      purchaseCommands,
    );

    expectErr(result, ApPurchaseOrderBillingSyncFailedError);
  });
});
