import { describe, expect, it } from "vitest";
import { createMockDb } from "../../../testing/index";
import type { DB } from "../generated/kysely-tailordb";
import { InvalidPaymentTermDefinitionError } from "../lib/errors.generated";
import { encodeJson, type DueScheduleLine, type PaymentTermScheduleLineInput } from "../lib/domain";
import {
  basePaymentTerm,
  installmentScheduleLines,
  endOfMonthScheduleLines,
} from "../testing/fixtures";
import { expectErr, expectOk } from "../testing/commandTestUtils";
import { run } from "./calculateInvoiceDueSchedule";

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

describe("calculateInvoiceDueSchedule", () => {
  it("calculates net-day due schedule", async () => {
    const { db, spies } = createMockDb<DB>();
    spies.select.mockReturnValueOnce(basePaymentTerm);

    const result = await run(db, {
      paymentTermId: basePaymentTerm.id,
      invoiceDate,
      invoiceAmount: 1000,
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result as never);

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

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

    const result = await run(db, {
      paymentTermId: installmentTerm.id,
      invoiceDate,
      invoiceAmount: 1000,
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result as never);

    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("calculates end-of-month due schedule", async () => {
    const { db, spies } = createMockDb<DB>();
    const eomTerm = {
      ...basePaymentTerm,
      id: "payment-term-eom",
      scheduleLines: encodeJson(endOfMonthScheduleLines),
    };
    spies.select.mockReturnValueOnce(eomTerm);

    const result = await run(db, {
      paymentTermId: eomTerm.id,
      invoiceDate,
      invoiceAmount: 500,
    });
    const { dueSchedule } = expectOk<{ dueSchedule: DueScheduleLine[] }>(result as never);

    expect(dueSchedule).toHaveLength(1);
    // End of February 2024 (leap year) → Feb 29
    expect(dueSchedule[0]).toMatchObject({
      amount: 500,
      dueDate: new Date("2024-02-29T00:00:00.000Z"),
    });
  });

  it("throws when payment term definition is invalid", async () => {
    const { db, spies } = createMockDb<DB>();
    const invalidLines: PaymentTermScheduleLineInput[] = [
      { kind: "INSTALLMENT", days: 15, percentage: 40 },
      { kind: "INSTALLMENT", days: 30, percentage: 40 },
    ];
    const invalidTerm = {
      ...basePaymentTerm,
      id: "payment-term-invalid",
      scheduleLines: encodeJson(invalidLines),
    };
    spies.select.mockReturnValueOnce(invalidTerm);

    const result = await run(db, {
      paymentTermId: invalidTerm.id,
      invoiceDate,
      invoiceAmount: 1000,
    });

    expectErr(result as never, InvalidPaymentTermDefinitionError);
  });
});
