import { Prisma } from "@jorgecuadros/database";
import {
  BALANCE_FLOOR_JOIN,
  BALANCE_FORWARD_TYPE,
  BillingService,
  NOT_SUPERSEDED,
} from "./billing.service";

/**
 * The balance floor drops rows a later BALANCE FORWARD already accounts for.
 *
 * It is worth testing because it fails silently: nothing throws, the numbers are
 * just wrong, and they were wrong for years — the whole book read +20.6M MXN in
 * credit because every customer's pre-cutover history was counted twice, once
 * inside their opening balance and once as itself.
 */
describe("balance floor", () => {
  describe("SQL fragments", () => {
    it("binds the type name rather than interpolating it", () => {
      // A literal would be a second place to edit if the label ever changes,
      // and this string reaches SQL from a module constant.
      expect(BALANCE_FLOOR_JOIN.values).toEqual([BALANCE_FORWARD_TYPE]);
    });

    it("keys the floor to the row's own customer", () => {
      // Without this the derived table cross-joins and every customer inherits
      // the earliest BALANCE FORWARD in the book.
      expect(BALANCE_FLOOR_JOIN.sql).toContain(
        "bfloor ON bfloor.customerId = t.customerId",
      );
    });

    it("takes the most recent opening balance, not the first", () => {
      // A customer accumulates one BALANCE FORWARD per year. MIN would floor at
      // the oldest and leave every intervening year double-counted.
      expect(BALANCE_FLOOR_JOIN.sql).toContain("MAX(bf.transactionDate)");
      expect(BALANCE_FLOOR_JOIN.sql).not.toContain("MIN(bf.transactionDate)");
    });

    it("ignores voided opening balances when locating the floor", () => {
      expect(BALANCE_FLOOR_JOIN.sql).toContain("bf.voidedAt IS NULL");
    });

    it("is inclusive of the opening balance row itself", () => {
      // `>` instead of `>=` would drop the carried balance and understate every
      // customer by exactly that amount.
      expect(NOT_SUPERSEDED.sql).toContain("t.transactionDate >= bfloor.floorDate");
      expect(NOT_SUPERSEDED.sql).not.toMatch(/transactionDate\s*>\s*bfloor/);
    });

    it("leaves customers with no opening balance untouched", () => {
      // NULL comparisons are never true, so without the explicit IS NULL branch
      // a customer who has no BALANCE FORWARD row loses their entire ledger.
      expect(NOT_SUPERSEDED.sql).toContain("bfloor.floorDate IS NULL");
    });

    it("only ever references the alias the join defines", () => {
      // The predicate is useless without the join; pairing them wrongly is a
      // runtime "unknown column", so keep the alias identical in both.
      const aliases = NOT_SUPERSEDED.sql.match(/bfloor\.\w+/g) ?? [];
      expect(aliases.length).toBeGreaterThan(0);
      for (const ref of aliases) {
        expect(BALANCE_FLOOR_JOIN.sql).toContain(ref.split(".")[1]);
      }
    });
  });

  describe("statement()", () => {
    /**
     * One customer means one floor date, so the statement uses a scalar lookup
     * instead of the join. Asserting on the `where` Prisma is handed is the only
     * way to see it without a database.
     */
    function serviceWith(floor: Date | null) {
      const findMany = jest.fn().mockResolvedValue([]);
      const prisma = {
        customer: {
          findUnique: jest.fn().mockResolvedValue({
            id: "c1",
            name: "CUADROS, JORGE H.",
            preferredCurrency: "USD",
            _count: { properties: 0, policies: 0 },
          }),
        },
        transaction: {
          findFirst: jest
            .fn()
            .mockResolvedValue(floor ? { transactionDate: floor } : null),
          findMany,
        },
      };

      return {
        service: new BillingService(prisma as never),
        prisma,
        findMany,
      };
    }

    it("looks the floor up from the customer's newest opening balance", async () => {
      const { service, prisma } = serviceWith(new Date("2026-01-01T00:00:00Z"));

      await service.statement("c1");

      expect(prisma.transaction.findFirst).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            customerId: "c1",
            voidedAt: null,
            type: { nameEn: BALANCE_FORWARD_TYPE },
          },
          orderBy: { transactionDate: "desc" },
          select: { transactionDate: true },
        }),
      );
    });

    it("bounds the statement at the floor, inclusive", async () => {
      const floor = new Date("2026-01-01T00:00:00Z");
      const { service, findMany } = serviceWith(floor);

      await service.statement("c1");

      expect(findMany.mock.calls[0][0].where).toMatchObject({
        customerId: "c1",
        transactionDate: { gte: floor },
      });
    });

    it("applies no date bound when the customer has no opening balance", async () => {
      const { service, findMany } = serviceWith(null);

      await service.statement("c1");

      expect(findMany.mock.calls[0][0].where).not.toHaveProperty(
        "transactionDate",
      );
    });

    it("keeps the source-table exclusion alongside the floor", async () => {
      // The two guards answer different questions — one reproduces legacy's
      // DATOS2-only materialization, the other drops superseded history — and
      // dropping either one changes the customer's balance.
      const { service, findMany } = serviceWith(new Date("2026-01-01T00:00:00Z"));

      await service.statement("c1");

      const where = findMany.mock.calls[0][0].where;
      expect(where.OR).toEqual([
        { legacySourceTable: null },
        { legacySourceTable: { notIn: expect.arrayContaining(["EFECTIVO"]) } },
      ]);
    });
  });

  describe("regression: NUMid 501", () => {
    /**
     * The arithmetic that exposed the bug, pinned so it cannot silently return.
     * Figures measured against the live ledger on 2026-08-05.
     */
    const openingBalance = new Prisma.Decimal("-6732.29");
    const activitySinceOpening = new Prisma.Decimal("-7333.00");
    const preCutoverCashAlreadyInOpening = new Prisma.Decimal("3596.00");

    it("matches the legacy portal once superseded rows are dropped", () => {
      expect(openingBalance.plus(activitySinceOpening).toFixed(2)).toBe(
        "-14065.29",
      );
    });

    it("reproduces the wrong figure when they are not", () => {
      expect(
        openingBalance
          .plus(activitySinceOpening)
          .plus(preCutoverCashAlreadyInOpening)
          .toFixed(2),
      ).toBe("-10469.29");
    });
  });
});
