import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  AccountCompanyMismatchError,
  AccountInactiveError,
  AccountNotFoundError,
  ApDueScheduleInvalidError,
  ApDocumentLineNotFoundError,
  ApDocumentNotFoundError,
  ApInvalidDocumentStatusError,
  ApLineAmountMismatchError,
  ApLineEditConflictError,
  ApLineSourceInconsistentError,
  ApLineTotalMismatchError,
  ApMinimumLinesNotMetError,
} from "../lib/errors.generated";
import { commandCtx, expectErr, expectOk } from "../testing/commandTestUtils";
import {
  baseAccountPayableDocument,
  baseAccountPayableDistributionLine,
  baseAccountPayableDocumentLine,
  baseCurrency,
  expenseAccount,
  payableAccount,
  postedAccountPayableDocument,
} from "../testing/fixtures";
import {
  mockCoaManagementQueries,
  mockPrimitivesQueries,
  type Account,
} from "../testing/moduleMocks";
import { run as runBase } from "./updateAccountPayableDocument";

const inactiveExpenseAccount: Account = {
  ...expenseAccount,
  id: "inactive-expense",
  status: "INACTIVE",
};
const inactivePayableAccount: Account = {
  ...payableAccount,
  id: "inactive-payable",
  status: "INACTIVE",
};
const otherCompanyPayableAccount: Account = {
  ...payableAccount,
  id: "other-company-payable",
  companyId: "company-2",
};
const defaultAccounts = [
  payableAccount,
  expenseAccount,
  inactiveExpenseAccount,
  inactivePayableAccount,
  otherCompanyPayableAccount,
];

const run = (
  db: Parameters<typeof runBase>[0],
  input: unknown,
  ctx: Parameters<typeof runBase>[2],
  accounts = defaultAccounts,
) =>
  runBase(
    db,
    input as Parameters<typeof runBase>[1],
    ctx,
    mockPrimitivesQueries({ currencies: [baseCurrency] }),
    mockCoaManagementQueries({ accounts }),
  );

describe("updateAccountPayableDocument", () => {
  it("updates DRAFT AP document header, lines, and distributions", async () => {
    const updated = { ...baseAccountPayableDocument, totalAmount: "1200" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [updated],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          headerPatch: { totalAmount: "1200", payableControlAccountId: payableAccount.id },
          updateLines: [
            {
              lineId: baseAccountPayableDocumentLine.id,
              patch: { grossAmount: "1200" },
              updateDistributions: [
                {
                  distributionLineId: baseAccountPayableDistributionLine.id,
                  patch: { amount: "1200" },
                },
              ],
            },
          ],
        },
        commandCtx,
      ),
    );

    expect(expectOk(result).accountPayableDocument.totalAmount).toBe("1200");
  });

  it("passes custom header and line fields through to writes", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
      [baseAccountPayableDocument],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          headerPatch: { reviewerUserId: "user-1" },
          updateLines: [
            { lineId: baseAccountPayableDocumentLine.id, patch: { allocationKey: "A" } },
          ],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.updates[0].updateValues()).toEqual(
      expect.objectContaining({ reviewerUserId: "user-1" }),
    );
    expect(mock.updates[1].updateValues()).toEqual(expect.objectContaining({ allocationKey: "A" }));
  });

  it("adds distributions under an existing line without changing the line patch", async () => {
    const currentDocument = { ...baseAccountPayableDocument, totalAmount: "1200" };
    const currentLine = { ...baseAccountPayableDocumentLine, grossAmount: "1200" };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([currentDocument], [currentLine], [baseAccountPayableDistributionLine]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          updateLines: [
            {
              lineId: baseAccountPayableDocumentLine.id,
              addDistributions: [{ accountId: expenseAccount.id, amount: "200" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectOk(result);
    expect(mock.inserts[0].insertRows()).toEqual([
      {
        accountPayableDocumentLineId: baseAccountPayableDocumentLine.id,
        accountId: expenseAccount.id,
        amount: "200",
        correctionOfDistributionLineId: null,
        description: null,
      },
    ]);
  });

  it("replaces due schedule with multiple due schedule lines", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

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

    expectOk(result);
    expect(mock.deletes[0].sql).toContain('"AccountPayableDueScheduleLine"');
    expect(mock.inserts[0].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 effective due schedule total does not match document total", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          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 AP document does not exist", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: "missing", headerPatch: { totalAmount: "1000" } }, commandCtx),
    );

    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 mock.withTx((trx) =>
      run(
        trx,
        { id: postedAccountPayableDocument.id, headerPatch: { totalAmount: "1000" } },
        commandCtx,
      ),
    );

    expectErr(result, ApInvalidDocumentStatusError);
  });

  it("returns error when removing an unknown line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAccountPayableDocument], [baseAccountPayableDocumentLine]);

    const result = await mock.withTx((trx) =>
      run(trx, { id: baseAccountPayableDocument.id, removeLineIds: ["missing-line"] }, commandCtx),
    );

    expectErr(result, ApDocumentLineNotFoundError);
  });

  it("returns error when updating a distribution outside the edited line", async () => {
    const secondLine = {
      ...baseAccountPayableDocumentLine,
      id: "ap-document-line-2",
    };
    const secondDistribution = {
      ...baseAccountPayableDistributionLine,
      id: "ap-distribution-line-2",
      accountPayableDocumentLineId: secondLine.id,
    };
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine, secondLine],
      [baseAccountPayableDistributionLine, secondDistribution],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          updateLines: [
            {
              lineId: baseAccountPayableDocumentLine.id,
              updateDistributions: [
                {
                  distributionLineId: secondDistribution.id,
                  patch: { amount: "900" },
                },
              ],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApDocumentLineNotFoundError);
  });

  it("returns error when updating and removing the same line", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          removeLineIds: [baseAccountPayableDocumentLine.id],
          updateLines: [
            {
              lineId: baseAccountPayableDocumentLine.id,
              addDistributions: [{ accountId: expenseAccount.id, amount: "1000" }],
            },
          ],
        },
        commandCtx,
      ),
    );

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

  it("returns error when effective lines are empty", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          removeLineIds: [baseAccountPayableDocumentLine.id],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApMinimumLinesNotMetError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          updateLines: [
            { lineId: baseAccountPayableDocumentLine.id, patch: { grossAmount: "900" } },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineTotalMismatchError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          updateLines: [
            {
              lineId: baseAccountPayableDocumentLine.id,
              updateDistributions: [
                {
                  distributionLineId: baseAccountPayableDistributionLine.id,
                  patch: { amount: "900" },
                },
              ],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineTotalMismatchError);
  });

  it("returns error when an added purchase-order sourced line omits the purchase order line reference", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults(
      [baseAccountPayableDocument],
      [baseAccountPayableDocumentLine],
      [baseAccountPayableDistributionLine],
    );

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          addLines: [
            {
              netAmount: "1000",
              grossAmount: "1000",
              sourceType: "PURCHASE_ORDER",
              quantity: "10",
              unitPrice: "100",
              distributions: [{ accountId: expenseAccount.id, amount: "1000" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineSourceInconsistentError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          addLines: [
            {
              netAmount: "900",
              grossAmount: "900",
              sourceType: "PURCHASE_ORDER",
              purchaseOrderLineId: "purchase-order-line-2",
              quantity: "10",
              unitPrice: "100",
              unitId: "unit-1",
              distributions: [{ accountId: expenseAccount.id, amount: "900" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, ApLineAmountMismatchError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          headerPatch: { totalAmount: "1100" },
          addLines: [
            {
              netAmount: "100",
              grossAmount: "100",
              distributions: [{ accountId: "missing", amount: "100" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountNotFoundError);
  });

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

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          headerPatch: { totalAmount: "1100" },
          addLines: [
            {
              netAmount: "100",
              grossAmount: "100",
              distributions: [{ accountId: inactiveExpenseAccount.id, amount: "100" }],
            },
          ],
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountInactiveError);
  });

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

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

    expectErr(result, AccountNotFoundError);
  });

  it("returns error when payable control account belongs to another company", async () => {
    const mock = createKyselyMock<Namespace["main-db"]>();
    mock.enqueueResults([baseAccountPayableDocument]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          id: baseAccountPayableDocument.id,
          headerPatch: { payableControlAccountId: otherCompanyPayableAccount.id },
        },
        commandCtx,
      ),
    );

    expectErr(result, AccountCompanyMismatchError);
  });

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

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

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