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

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

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

    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 discount window and final due date for 2/10 net 30", async () => {
    const { db, spies } = createMockDb<DB>();
    const discountTerm = {
      ...basePaymentTerm,
      id: "payment-term-discount",
      scheduleLines: encodeJson(discountScheduleLines),
    };
    spies.select.mockReturnValueOnce(discountTerm);

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

    expect(dueSchedule).toHaveLength(1);
    expect(dueSchedule[0]).toMatchObject({
      amount: 1000,
      // bill date + 30 days
      dueDate: new Date("2024-03-16T00:00:00.000Z"),
      // bill date + 10 days
      discountDueDate: new Date("2024-02-25T00:00:00.000Z"),
      // 2% of 1000
      discountAmount: 20,
    });
  });

  it("calculates multiple due-schedule lines for installment terms", 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, {
      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: PaymentTermScheduleLineInput[] = [{ kind: "END_OF_MONTH" }];
    const eomTerm = {
      ...basePaymentTerm,
      id: "payment-term-eom",
      scheduleLines: encodeJson(eomLines),
    };
    spies.select.mockReturnValueOnce(eomTerm);

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

    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("assigns rounding residual to the final schedule line", async () => {
    const { db, spies } = createMockDb<DB>();
    // 33.33 + 33.33 + 33.34 = 100, but per-line rounding on an odd amount creates residual
    const threeWayLines: PaymentTermScheduleLineInput[] = [
      { kind: "INSTALLMENT", days: 10, percentage: 33.33 },
      { kind: "INSTALLMENT", days: 20, percentage: 33.33 },
      { kind: "INSTALLMENT", days: 30, percentage: 33.34 },
    ];
    const threeWayTerm = {
      ...basePaymentTerm,
      id: "payment-term-3way",
      scheduleLines: encodeJson(threeWayLines),
    };
    spies.select.mockReturnValueOnce(threeWayTerm);

    // Use an amount that causes actual rounding residual: 999
    // 33.33% of 999 = 333.0367 → 333.04, 33.33% of 999 = 333.04, 33.34% of 999 = 333.0666 → 333.07
    // Sum = 333.04 + 333.04 + 333.07 = 999.15, residual = 999 - 999.15 = -0.15
    // Final line absorbs residual: 333.07 + (-0.15) = 332.92
    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 + 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 payment term configuration 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, {
      purchasePaymentTermId: invalidTerm.id,
      billDate,
      billAmount: 1000,
    });

    expectErr(result, InvalidPaymentTermConfigurationError);
  });
});
