import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountInactiveError,
  AccountNotFoundError,
  ApBusinessPartnerNotFoundError,
  ApCurrencyNotFoundError,
  ApDueScheduleInvalidError,
  ApInvalidAmountError,
  ApLineAmountMismatchError,
  ApLineSourceInconsistentError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
  CompanyNotFoundError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccountPayableDocument,
  baseAccountPayableDocumentLine,
  baseBusinessPartner,
  baseCompany,
  baseCurrency,
  expenseAccount,
  payableAccount,
} from "../testing/fixtures";
import {
  mockBusinessPartnerQueries,
  mockCoaManagementQueries,
  mockOrganizationQueries,
  mockPrimitivesQueries,
  type Account,
  type Company,
  type Currency,
  type Partner,
} from "../testing/moduleMocks";
import {
  run as runBase,
  type CreateAccountPayableDocumentInput,
} from "./createAccountPayableDocument";

const input: CreateAccountPayableDocumentInput = {
  header: {
    companyId: baseCompany.id,
    businessPartnerId: baseBusinessPartner.id,
    currencyId: baseCurrency.id,
    payableControlAccountId: payableAccount.id,
    documentType: "INVOICE",
    externalDocumentNumber: "VEND-1001",
    documentDate: new Date("2024-01-10"),
    postingDate: new Date("2024-01-15"),
    totalAmount: "1000",
    description: "Vendor invoice",
  },
  lines: [
    {
      netAmount: "1000",
      grossAmount: "1000",
      distributions: [
        {
          accountId: expenseAccount.id,
          amount: "1000",
          description: "Expense distribution",
        },
      ],
      description: "Purchase order line",
      sourceType: "PURCHASE_ORDER",
      purchaseOrderLineId: "purchase-order-line-1",
      quantity: "10",
      unitPrice: "100",
      unitId: "unit-1",
    },
  ],
};

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 run = (
  db: Parameters<typeof runBase>[0],
  runInput: unknown,
  ctx: Parameters<typeof runBase>[2],
  companies: Company[] = [baseCompany],
  partners: Partner[] = [baseBusinessPartner],
  currencies: Currency[] = [baseCurrency],
) =>
  runBase(
    db,
    runInput as Parameters<typeof runBase>[1],
    ctx,
    mockOrganizationQueries({ companies }),
    mockBusinessPartnerQueries({ partners }),
    mockPrimitivesQueries({ currencies }),
    mockCoaManagementQueries({ accounts: defaultAccounts }),
  );

function enqueueValidCreate(mock: ReturnType<typeof createKyselyMock<Namespace["main-db"]>>) {
  mock.enqueueResults([baseAccountPayableDocument], [baseAccountPayableDocumentLine]);
}

describe("createAccountPayableDocument", () => {
  it("creates DRAFT AP document with lines and distributions", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock);

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

    const value = expectOk(result);
    expect(value.accountPayableDocument.status).toBe("DRAFT");
    expect(mock.inserts[0].insertRows()).toEqual([expect.objectContaining({ status: "DRAFT" })]);
    expect(mock.inserts[1].insertRows()).toEqual([
      expect.objectContaining({ accountPayableDocumentId: baseAccountPayableDocument.id }),
    ]);
    expect(mock.inserts[2].insertRows()).toEqual([
      expect.objectContaining({
        accountPayableDocumentLineId: baseAccountPayableDocumentLine.id,
        accountId: expenseAccount.id,
      }),
    ]);
  });

  it("passes custom header and line fields through to inserts", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          header: { ...input.header, customApprovalCode: "A1" },
          lines: [{ ...input.lines[0], customCostCenter: "CC-10" }],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()).toEqual([
      expect.objectContaining({ customApprovalCode: "A1" }),
    ]);
    expect(mock.inserts[1].insertRows()).toEqual([
      expect.objectContaining({ customCostCenter: "CC-10" }),
    ]);
  });

  it("creates DRAFT AP document with multiple due schedule lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    enqueueValidCreate(mock);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          dueSchedule: [
            { dueDate: new Date("2024-02-15"), amount: "400" },
            { dueDate: new Date("2024-03-15"), amount: "600" },
          ],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.inserts[3].insertRows()).toEqual([
      {
        accountPayableDocumentId: baseAccountPayableDocument.id,
        dueDate: new Date("2024-02-15"),
        amount: "400",
      },
      {
        accountPayableDocumentId: baseAccountPayableDocument.id,
        dueDate: new Date("2024-03-15"),
        amount: "600",
      },
    ]);
  });

  it("returns error when due schedule total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          dueSchedule: [
            { dueDate: new Date("2024-02-15"), amount: "400" },
            { dueDate: new Date("2024-03-15"), amount: "500" },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApDueScheduleInvalidError);
  });

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

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

    expectErr(result, CompanyNotFoundError);
  });

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

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

    expectErr(result, ApBusinessPartnerNotFoundError);
  });

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

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

    expectErr(result, ApCurrencyNotFoundError);
  });

  it("returns error when no lines are provided", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

    expectErr(result, ApMinimumLinesNotMetError);
  });

  it("returns error when total amount is not positive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

    expectErr(result, ApInvalidAmountError);
  });

  it("returns error when line gross total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when distribution total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when a purchase-order sourced line omits the purchase order line reference", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          lines: [{ ...input.lines[0], purchaseOrderLineId: null }],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineSourceInconsistentError);
  });

  it("returns error when a purchase-order sourced line is missing a unit", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          lines: [{ ...input.lines[0], unitId: null }],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineSourceInconsistentError);
  });

  it("returns error when a purchase-order sourced line net amount does not equal quantity times unit price", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

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

    expectErr(result, ApLineAmountMismatchError);
  });

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

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

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when distribution account is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    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);
  });

  it("returns error when payable control account does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          header: { ...input.header, payableControlAccountId: "missing-account" },
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when payable control account is inactive", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          ...input,
          header: { ...input.header, payableControlAccountId: inactivePayableAccount.id },
        },
        commandCtx,
      ),
    );

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