import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  ApDistributionLineNotFoundError,
  ApDocumentLineNotFoundError,
  ApInvalidDocumentStatusError,
  ApLineSourceInconsistentError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccountPayableDistributionLine,
  baseAccountPayableDocument,
  baseAccountPayableDocumentLine,
  baseBusinessPartner,
  baseCompany,
  baseCurrency,
  expenseAccount,
  payableAccount,
  postedAccountPayableDocument,
} from "../testing/fixtures";
import {
  mockBusinessPartnerQueries,
  mockCoaManagementQueries,
  mockOrganizationQueries,
  mockPrimitivesQueries,
  type Account,
} from "../testing/moduleMocks";
import {
  run as runBase,
  type CorrectAccountPayableDocumentInput,
} from "./correctAccountPayableDocument";

const correctionDocument = {
  ...baseAccountPayableDocument,
  id: "ap-correction-1",
  documentType: "CREDIT_MEMO",
  correctionOfId: postedAccountPayableDocument.id,
  totalAmount: "20",
} as const;

const correctionLine = {
  ...baseAccountPayableDocumentLine,
  id: "ap-correction-line-1",
  accountPayableDocumentId: correctionDocument.id,
  netAmount: "20",
  grossAmount: "20",
  correctionOfLineId: baseAccountPayableDocumentLine.id,
  correctionType: "PRICE",
  unitPrice: "2",
} as const;

const input: CorrectAccountPayableDocumentInput = {
  correctionOfId: postedAccountPayableDocument.id,
  header: {
    documentType: "CREDIT_MEMO",
    externalDocumentNumber: "VEND-CM-1001",
    documentDate: new Date("2024-02-10"),
    postingDate: new Date("2024-02-15"),
    totalAmount: "20",
    description: "Price correction",
  },
  lines: [
    {
      correctionOfLineId: baseAccountPayableDocumentLine.id,
      correctionType: "PRICE",
      netAmount: "20",
      grossAmount: "20",
      quantity: "10",
      unitPrice: "2",
      distributions: [
        {
          correctionOfDistributionLineId: baseAccountPayableDistributionLine.id,
          accountId: expenseAccount.id,
          amount: "20",
        },
      ],
    },
  ],
};

const inactiveExpenseAccount: Account = {
  ...expenseAccount,
  id: "inactive-expense",
  status: "INACTIVE",
};
const defaultAccounts = [payableAccount, expenseAccount, inactiveExpenseAccount];

const run = (
  db: Parameters<typeof runBase>[0],
  runInput: unknown,
  ctx: Parameters<typeof runBase>[2],
  accounts = defaultAccounts,
) =>
  runBase(
    db,
    runInput as Parameters<typeof runBase>[1],
    ctx,
    mockOrganizationQueries({ companies: [baseCompany] }),
    mockBusinessPartnerQueries({ partners: [baseBusinessPartner] }),
    mockPrimitivesQueries({ currencies: [baseCurrency] }),
    mockCoaManagementQueries({ accounts }),
  );

function enqueueValidCorrection(mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>) {
  mock.enqueueResults(
    [postedAccountPayableDocument],
    [baseAccountPayableDocumentLine],
    [baseAccountPayableDistributionLine],
    [correctionDocument],
    [correctionLine],
  );
}

describe("correctAccountPayableDocument", () => {
  it("creates a DRAFT correction document with line and distribution correction links", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCorrection(mock);

    const result = await mock.withTx((trx) => run(trx, input, commandCtx));

    const value = expectOk(result);
    expect(value.accountPayableDocument.id).toBe(correctionDocument.id);
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        correctionOfId: postedAccountPayableDocument.id,
        documentType: "CREDIT_MEMO",
        companyId: postedAccountPayableDocument.companyId,
        businessPartnerId: postedAccountPayableDocument.businessPartnerId,
        currencyId: postedAccountPayableDocument.currencyId,
      }),
    ]);
    expect(mock.inserts[1].insertRows()).toEqual([
      expect.objectContaining({
        correctionOfLineId: baseAccountPayableDocumentLine.id,
        correctionType: "PRICE",
        sourceType: "PURCHASE_ORDER",
        purchaseOrderLineId: baseAccountPayableDocumentLine.purchaseOrderLineId,
      }),
    ]);
    expect(mock.inserts[2].insertRows()).toEqual([
      expect.objectContaining({
        correctionOfDistributionLineId: baseAccountPayableDistributionLine.id,
      }),
    ]);
  });

  it("creates an INVOICE correction for an AP-increasing correction", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCorrection(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          header: {
            ...input.header,
            documentType: "INVOICE",
          },
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({
        correctionOfId: postedAccountPayableDocument.id,
        documentType: "INVOICE",
      }),
    ]);
  });

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

    const result = await mock.withTx((trx) => run(trx, input, commandCtx));

    expectErr(result, ApInvalidDocumentStatusError);
  });

  it("returns error when a correction line references a line outside the corrected document", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([postedAccountPayableDocument], [], []);

    const result = await mock.withTx((trx) => run(trx, input, commandCtx));

    expectErr(result, ApDocumentLineNotFoundError);
  });

  it("returns error when a correction distribution references another line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [
        {
          ...baseAccountPayableDistributionLine,
          accountPayableDocumentLineId: "other-line",
        },
      ],
    );

    const result = await mock.withTx((trx) => run(trx, input, commandCtx));

    expectErr(result, ApDistributionLineNotFoundError);
  });

  it("returns error when a price correction targets a non-PO source line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedAccountPayableDocument],
      [
        {
          ...baseAccountPayableDocumentLine,
          sourceType: null,
          purchaseOrderLineId: null,
          quantity: null,
          unitPrice: null,
          unitId: null,
        },
      ],
      [],
    );

    const result = await mock.withTx((trx) => run(trx, input, commandCtx));

    expectErr(result, ApLineSourceInconsistentError);
  });

  it("returns error when a correction line changes the original purchase order source", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [postedAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          lines: [
            {
              ...input.lines[0],
              purchaseOrderLineId: "different-purchase-order-line",
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineSourceInconsistentError);
  });

  it("returns missing account errors through the shared AP document validation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCorrection(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          lines: [
            {
              ...input.lines[0],
              distributions: [{ ...input.lines[0].distributions[0], accountId: "missing" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

  it("returns inactive account errors through the shared AP document validation", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCorrection(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          lines: [
            {
              ...input.lines[0],
              distributions: [
                { ...input.lines[0].distributions[0], accountId: inactiveExpenseAccount.id },
              ],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountInactiveError);
  });
});
