import { Prisma } from "@jorgecuadros/database";
import { BillingService } from "./billing.service";

/**
 * The statement is a *year* statement, like the EDO CUENTA report the office
 * prints: this year's movements, oldest-first, opening on the balance carried
 * in from before it.
 *
 * The carrying is the part worth testing. Dropping earlier rows from the list
 * is easy; dropping them from the arithmetic too would restart every balance at
 * zero on January 1st, and nothing would throw — the numbers would just be
 * wrong, which is exactly how the double-counting bug lived for years.
 */
describe("statement year scoping", () => {
  const YEAR = new Date().getUTCFullYear();

  function d(iso: string) {
    return new Date(`${iso}T00:00:00.000Z`);
  }

  type RowSpec = {
    id: string;
    date: Date;
    amount: string;
    currency?: string;
    domain?: string;
    voidedAt?: Date | null;
    outstanding?: boolean;
  };

  function row(r: RowSpec) {
    return {
      id: r.id,
      transactionDate: r.date,
      domain: r.domain ?? "UTILITY",
      amount: new Prisma.Decimal(r.amount),
      currency: r.currency ?? "MXN",
      reference: null,
      period: null,
      checkNumber: null,
      message: null,
      legacySourceTable: null,
      voidedAt: r.voidedAt ?? null,
      outstanding: r.outstanding ?? false,
      type: { nameEn: "WATER", nameEs: "AGUA" },
    };
  }

  /** No BALANCE FORWARD row, so the floor is null and every row is fetched. */
  function serviceWith(rows: RowSpec[]) {
    const prisma = {
      customer: {
        findUnique: jest.fn().mockResolvedValue({
          id: "c1",
          name: "CUADROS, JORGE H.",
          preferredCurrency: "MXN",
          _count: { properties: 0, policies: 0 },
        }),
      },
      transaction: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue(rows.map(row)),
      },
    };
    return new BillingService(prisma as never);
  }

  it("lists the year's movements oldest-first", async () => {
    const s = await serviceWith([
      { id: "a", date: d(`${YEAR}-01-02`), amount: "-100" },
      { id: "b", date: d(`${YEAR}-03-04`), amount: "250" },
      { id: "c", date: d(`${YEAR}-07-16`), amount: "-40" },
    ]).statement("c1");

    expect(s.movements.map((m) => m.id)).toEqual(["a", "b", "c"]);
  });

  it("leaves earlier years off the list", async () => {
    const s = await serviceWith([
      { id: "old", date: d(`${YEAR - 1}-11-30`), amount: "-500" },
      { id: "new", date: d(`${YEAR}-02-11`), amount: "-100" },
    ]).statement("c1");

    expect(s.movements.map((m) => m.id)).toEqual(["new"]);
  });

  it("carries the earlier years' balance instead of discarding it", async () => {
    // 1,000 credit left over from last year, 300 charged this year: the
    // customer is 700 in credit, not 300 in debt.
    const s = await serviceWith([
      { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000" },
      { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" },
    ]).statement("c1");

    const mxn = s.summary.find((x) => x.currency === "MXN");
    expect(mxn?.opening).toBe("1000.00");
    expect(mxn?.charges).toBe("-300.00");
    expect(mxn?.balance).toBe("700.00");
    // The running balance on the listed row picks up where last year left off.
    expect(s.movements[0].balanceAfter).toBe("700.00");
  });

  it("carries it per business line as well", async () => {
    const s = await serviceWith([
      { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000", domain: "INSURANCE" },
      { id: "new", date: d(`${YEAR}-02-11`), amount: "-300", domain: "INSURANCE" },
    ]).statement("c1");

    const line = s.byDomain.find((x) => x.domain === "INSURANCE");
    expect(line?.opening).toBe("1000.00");
    expect(line?.balance).toBe("700.00");
  });

  it("still reports a currency that only moved in earlier years", async () => {
    // Otherwise a customer sitting on a dollar credit they haven't touched all
    // year would appear to have no dollar balance at all.
    const s = await serviceWith([
      { id: "old", date: d(`${YEAR - 2}-05-01`), amount: "180.83", currency: "USD" },
      { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" },
    ]).statement("c1");

    const usd = s.summary.find((x) => x.currency === "USD");
    expect(usd?.balance).toBe("180.83");
    expect(usd?.count).toBe(0);
  });

  it("does not carry a voided earlier row", async () => {
    const s = await serviceWith([
      { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000", voidedAt: d(`${YEAR - 1}-12-16`) },
      { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" },
    ]).statement("c1");

    const mxn = s.summary.find((x) => x.currency === "MXN");
    expect(mxn?.opening).toBe("0.00");
    expect(mxn?.balance).toBe("-300.00");
  });

  it("reports the year it covers", async () => {
    const s = await serviceWith([
      { id: "a", date: d(`${YEAR}-01-02`), amount: "-100" },
    ]).statement("c1");

    expect(s.year).toBe(YEAR);
  });
});
