import { err } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import { CompanyNotFoundError } from "../../accounting-event/lib/errors.generated";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  ApAccountingEventPostFailedError,
  ApCurrencyNotFoundError,
  ApDocumentHasActiveHoldError,
  ApDocumentNotFoundError,
  ApInvalidDocumentStatusError,
  ApLineTotalMismatchError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccountPayableDistributionLine,
  baseAccountPayableDueScheduleLine,
  baseAccountPayableDocumentLine,
  baseCurrency,
  creditMemoAccountPayableDocument,
  expenseAccount,
  payableAccount,
  postedAccountPayableDocument,
  registeredAccountPayableDocument,
} from "../testing/fixtures";
import {
  mockAccountingEventCommands,
  mockCoaManagementQueries,
  mockPrimitivesQueries,
  type Account,
} from "../testing/moduleMocks";
import { run as runBase } from "./postAccountPayableDocument";

const input = {
  id: registeredAccountPayableDocument.id,
};

const inactiveExpenseAccount: Account = {
  ...expenseAccount,
  id: "inactive-expense",
  status: "INACTIVE",
};
const inactivePayableAccount: Account = {
  ...payableAccount,
  id: "inactive-payable",
  status: "INACTIVE",
};
const defaultAccounts = [
  payableAccount,
  expenseAccount,
  inactiveExpenseAccount,
  inactivePayableAccount,
];
const secondAccountPayableDocumentLine = {
  ...baseAccountPayableDocumentLine,
  id: "ap-document-line-2",
  grossAmount: "300",
  netAmount: "300",
  quantity: "3",
};
const splitAccountPayableDocumentLine = {
  ...baseAccountPayableDocumentLine,
  grossAmount: "700",
  netAmount: "700",
  quantity: "7",
};
const secondAccountPayableDistributionLine = {
  ...baseAccountPayableDistributionLine,
  id: "ap-distribution-line-2",
  accountPayableDocumentLineId: secondAccountPayableDocumentLine.id,
  amount: "300",
};

const run = (
  db: Parameters<typeof runBase>[0],
  runInput: unknown,
  ctx: Parameters<typeof runBase>[2],
  primitivesQueries: Parameters<typeof runBase>[3],
  accountingEventCommands: Parameters<typeof runBase>[4],
  accounts = defaultAccounts,
) =>
  runBase(
    db,
    runInput as Parameters<typeof runBase>[1],
    ctx,
    primitivesQueries,
    accountingEventCommands,
    mockCoaManagementQueries({ accounts }),
  );

function enqueueValidPost(
  mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>,
  document:
    | typeof registeredAccountPayableDocument
    | typeof creditMemoAccountPayableDocument = registeredAccountPayableDocument,
) {
  mock.enqueueResults(
    [document],
    [baseAccountPayableDocumentLine],
    [], // active holds
    [baseAccountPayableDistributionLine],
    [baseAccountPayableDueScheduleLine],
    [{ ...postedAccountPayableDocument, id: document.id }],
  );
}

describe("postAccountPayableDocument", () => {
  it("posts REGISTERED invoice AP document and emits an accounting event", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const accountingEventCommands = mockAccountingEventCommands();
    enqueueValidPost(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        accountingEventCommands,
      ),
    );

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("POSTED");
    expect(value.accountingEvent.id).toBe("accounting-event-1");
    const postedInput = accountingEventCommands.postAccountingEvent.mock.lastCall?.[1];
    expect(postedInput).toEqual(
      expect.objectContaining({
        eventKind: "ACCOUNT_PAYABLE_RECOGNIZED",
        schemaVersion: "1",
      }),
    );
    if (!postedInput) throw new Error("expected accounting event input");
    const payload = postedInput.payload;
    expect(payload).toEqual(
      expect.objectContaining({
        source: {
          documentType: "ACCOUNT_PAYABLE_DOCUMENT",
          documentId: registeredAccountPayableDocument.id,
          documentNumber: registeredAccountPayableDocument.documentNumber,
        },
        documentType: "INVOICE",
        supplierId: registeredAccountPayableDocument.businessPartnerId,
        currencyCode: baseCurrency.code,
        payableAccountId: payableAccount.id,
      }),
    );
    expect(payload.dueSchedule).toEqual([
      {
        dueDate: baseAccountPayableDueScheduleLine.dueDate,
        amount: baseAccountPayableDueScheduleLine.amount,
      },
    ]);
    expect(payload.lines).toEqual([
      expect.objectContaining({
        lineId: baseAccountPayableDocumentLine.id,
        amount: baseAccountPayableDocumentLine.grossAmount,
      }),
    ]);
    expect(payload.distributions).toEqual([
      expect.objectContaining({
        distributionId: baseAccountPayableDistributionLine.id,
        lineId: baseAccountPayableDistributionLine.accountPayableDocumentLineId,
        accountId: expenseAccount.id,
        amount: baseAccountPayableDistributionLine.amount,
      }),
    ]);
  });

  it("posts REGISTERED credit memo AP document and emits an accounting event", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    const accountingEventCommands = mockAccountingEventCommands();
    enqueueValidPost(mock, creditMemoAccountPayableDocument);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, id: creditMemoAccountPayableDocument.id },
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        accountingEventCommands,
      ),
    );

    expectOk(result);
    const payload = accountingEventCommands.postAccountingEvent.mock.lastCall?.[1]?.payload;
    if (!payload) throw new Error("expected accounting event payload");
    expect(payload.documentType).toBe("CREDIT_MEMO");
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [expenseAccount],
      ),
    );

    expectErr(result, ApDocumentNotFoundError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        { ...input, id: postedAccountPayableDocument.id },
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [expenseAccount],
      ),
    );

    expectErr(result, ApInvalidDocumentStatusError);
  });

  it("returns error when AP document has an active payment hold", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [
        {
          id: "ap-hold-1",
          accountPayableDocumentId: registeredAccountPayableDocument.id,
          accountPayableDocumentLineId: null,
          reasonCode: "INVOICE_UNIT_PRICE_DIFFERS_FROM_PURCHASE_ORDER_UNIT_PRICE",
          source: "SYSTEM",
          releasedAt: null,
          releaseReason: null,
          createdAt: new Date("2024-01-01T00:00:00.000Z"),
          updatedAt: null,
        },
      ], // active holds
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [expenseAccount],
      ),
    );

    expectErr(result, ApDocumentHasActiveHoldError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [expenseAccount],
      ),
    );

    expectErr(result, ApLineTotalMismatchError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
      ),
    );

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when a line distribution total does not match line gross amount", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [splitAccountPayableDocumentLine, secondAccountPayableDocumentLine],
      [], // active holds
      [
        { ...baseAccountPayableDistributionLine, amount: "1000" },
        { ...secondAccountPayableDistributionLine, amount: "0" },
      ],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
      ),
    );

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when payable control account does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [], // active holds
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [expenseAccount],
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when payable control account is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [], // active holds
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [{ ...inactivePayableAccount, id: payableAccount.id }, expenseAccount],
      ),
    );

    expectErr(result, AccountInactiveError);
  });

  it("returns error when distribution account is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [], // active holds
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        mockAccountingEventCommands(),
        [payableAccount, { ...inactiveExpenseAccount, id: expenseAccount.id }],
      ),
    );

    expectErr(result, AccountInactiveError);
  });

  it("returns error when currency does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [], // active holds
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [] }),
        mockAccountingEventCommands(),
      ),
    );

    expectErr(result, ApCurrencyNotFoundError);
    expect(mock.inserts).toHaveLength(0);
  });

  it("returns error when accounting event posting fails", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [registeredAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [], // active holds
      [baseAccountPayableDistributionLine],
      [],
    );
    const accountingEventCommands = mockAccountingEventCommands();
    accountingEventCommands.postAccountingEvent.mockResolvedValueOnce(
      err(new CompanyNotFoundError(registeredAccountPayableDocument.companyId)),
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        input,
        commandCtx,
        mockPrimitivesQueries({ currencies: [baseCurrency] }),
        accountingEventCommands,
      ),
    );

    expectErr(result, ApAccountingEventPostFailedError);
  });
});
