import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { DB } from "../generated/kysely-tailordb";
import type { DueScheduleLine } from "../lib/domain";
import {
  InvalidBillAmountError,
  InvalidPaymentTermConfigurationError,
  PaymentTermNotFoundError,
} from "../lib/errors.generated";
import { expectErr, expectOk } from "../testing/commandTestUtils";
import {
  basePaymentTerm,
  basePaymentTermLine,
  installmentScheduleLines,
  nullLineFields,
} from "../testing/fixtures";
import { run } from "./calculatePurchaseBillDueSchedule";

const billDate = new Date("2024-02-15T00:00:00.000Z");
const createdAt = new Date("2024-01-01T00:00:00.000Z");

// oxlint-disable-next-line typescript/no-explicit-any -- test helper with flexible input
function toDbLines(lines: Record<string, any>[], termId: string) {
  return lines.map((line, i: number) => ({
    id: `line-${i}`,
    purchasePaymentTermId: termId,
    ...line,
    createdAt,
    updatedAt: null,
  }));
}

describe("calculatePurchaseBillDueSchedule", () => {
  it("calculates a single due date for net-30 terms", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(basePaymentTerm);
    spies.select.mockReturnValueOnce([basePaymentTermLine]);

    const result = await run(db, {
      purchasePaymentTermId: basePaymentTerm.id,
      billDate,
      billAmount: "1000",
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result);

    expect(dueSchedule).toHaveLength(1);
    expect(dueSchedule[0]).toMatchObject({
      amount: "1000",
      dueDate: new Date("2024-03-16T00:00:00.000Z"),
    });
  });

  it("calculates multiple due-schedule lines for installment terms", async () => {
    const { db, spies } = createMockDb<DB>();
    const installmentTerm = { ...basePaymentTerm, id: "payment-term-installment" };
    spies.select.mockReturnValueOnce(installmentTerm);
    spies.select.mockReturnValueOnce(toDbLines(installmentScheduleLines, installmentTerm.id));

    const result = await run(db, {
      purchasePaymentTermId: installmentTerm.id,
      billDate,
      billAmount: "1000",
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result);

    expect(dueSchedule).toHaveLength(2);
    expect(dueSchedule[0]).toMatchObject({
      amount: "500",
      dueDate: new Date("2024-03-01T00:00:00.000Z"),
    });
    expect(dueSchedule[1]).toMatchObject({
      amount: "500",
      dueDate: new Date("2024-03-16T00:00:00.000Z"),
    });
  });

  it("applies end-of-month logic correctly", async () => {
    const { db, spies } = createMockDb<DB>();
    const eomLines = [{ ...nullLineFields, kind: "END_OF_MONTH" as const, days: null }];
    const eomTerm = { ...basePaymentTerm, id: "payment-term-eom" };
    spies.select.mockReturnValueOnce(eomTerm);
    spies.select.mockReturnValueOnce(toDbLines(eomLines, eomTerm.id));

    const result = await run(db, {
      purchasePaymentTermId: eomTerm.id,
      billDate,
      billAmount: "500",
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result);

    expect(dueSchedule).toHaveLength(1);
    expect(dueSchedule[0]).toMatchObject({
      amount: "500",
      dueDate: new Date("2024-02-29T00:00:00.000Z"),
    });
  });

  it("assigns rounding residual to the final schedule line", async () => {
    const { db, spies } = createMockDb<DB>();
    const threeWayLines = [
      { ...nullLineFields, kind: "INSTALLMENT" as const, days: 10, percentage: "33.33" },
      { ...nullLineFields, kind: "INSTALLMENT" as const, days: 20, percentage: "33.33" },
      { ...nullLineFields, kind: "INSTALLMENT" as const, days: 30, percentage: "33.34" },
    ];
    const threeWayTerm = { ...basePaymentTerm, id: "payment-term-3way" };
    spies.select.mockReturnValueOnce(threeWayTerm);
    spies.select.mockReturnValueOnce(toDbLines(threeWayLines, threeWayTerm.id));

    const result = await run(db, {
      purchasePaymentTermId: threeWayTerm.id,
      billDate,
      billAmount: "999",
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result);

    expect(dueSchedule).toHaveLength(3);
    const total = dueSchedule.reduce((sum, line) => sum + Number(line.amount), 0);
    expect(total).toBeCloseTo(999, 2);
  });

  it("returns error when payment term does not exist", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(undefined);

    const result = await run(db, {
      purchasePaymentTermId: "nonexistent-term",
      billDate,
      billAmount: "1000",
    });

    expectErr(result, PaymentTermNotFoundError);
  });

  it("returns error when bill amount is negative", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(basePaymentTerm);
    spies.select.mockReturnValueOnce([basePaymentTermLine]);

    const result = await run(db, {
      purchasePaymentTermId: basePaymentTerm.id,
      billDate,
      billAmount: "-500",
    });

    expectErr(result, InvalidBillAmountError);
  });

  it("returns error when payment term configuration is invalid", async () => {
    const { db, spies } = createMockDb<DB>();
    const invalidLines = [
      { ...nullLineFields, kind: "INSTALLMENT" as const, days: 15, percentage: "40" },
      { ...nullLineFields, kind: "INSTALLMENT" as const, days: 30, percentage: "40" },
    ];
    const invalidTerm = { ...basePaymentTerm, id: "payment-term-invalid" };
    spies.select.mockReturnValueOnce(invalidTerm);
    spies.select.mockReturnValueOnce(toDbLines(invalidLines, invalidTerm.id));

    const result = await run(db, {
      purchasePaymentTermId: invalidTerm.id,
      billDate,
      billAmount: "1000",
    });

    expectErr(result, InvalidPaymentTermConfigurationError);
  });

  it("calculates due schedule from snapshot lines without DB lookup", async () => {
    const { db } = createMockDb<DB>();

    const result = await run(db, {
      paymentTermSnapshotLines: [{ ...nullLineFields, kind: "NET_DAYS" as const, days: 30 }],
      billDate,
      billAmount: "750",
    });
    const { dueSchedule, paymentTerm } = expectOk<{
      dueSchedule: DueScheduleLine[];
      paymentTerm: unknown;
    }>(result);

    expect(dueSchedule).toHaveLength(1);
    expect(dueSchedule[0]).toMatchObject({
      amount: "750",
      dueDate: new Date("2024-03-16T00:00:00.000Z"),
    });
    expect(paymentTerm).toBeNull();
  });
});
