import { beforeEach, describe, expect, mock, test } from "bun:test";
import { PaymentStatus } from "../src/generated/prisma/client";

const mockWalletFindUnique = mock();
const mockWalletUpdate = mock();
const mockMovementCreate = mock();
const mockTransaction = mock(async (operations: Array<Promise<unknown>>) =>
  Promise.all(operations),
);

const mockPrisma = {
  $transaction: mockTransaction,
  periodicExpense: {
    findMany: mock(),
    findUniqueOrThrow: mock(),
    create: mock(),
    update: mock(),
  },
  expense: {
    findMany: mock(),
    findFirst: mock(),
    findUniqueOrThrow: mock(),
    create: mock(),
    update: mock(),
    count: mock(),
  },
  wallet: {
    findUnique: mockWalletFindUnique,
    update: mockWalletUpdate,
  },
  walletMovement: {
    create: mockMovementCreate,
  },
  quote: {
    findFirst: mock(),
  },
  quoteHistory: {
    findFirst: mock(),
  },
};

mock.module("../src/lib/prisma", () => ({
  prisma: mockPrisma,
}));

beforeEach(() => {
  for (const model of Object.values(mockPrisma)) {
    if (typeof model === "function") continue;
    for (const fn of Object.values(
      model as Record<string, ReturnType<typeof mock>>,
    )) {
      fn.mockClear();
    }
  }
  mockPrisma.expense.count.mockResolvedValue(0);
});

const {
  createPeriodicExpenseSchema,
  createNonPeriodicExpenseSchema,
  payExpenseSchema,
  updateExpenseSchema,
  buildDueDate,
  getCurrentYearMonthUTC,
  getBeloSellPriceForDate,
  listPeriodicExpenses,
  createPeriodicExpense,
  updatePeriodicExpense,
  softDeletePeriodicExpense,
  listExpenses,
  createNonPeriodicExpense,
  payExpense,
  updateExpense,
  generateExpensesForCurrentMonth,
  generateMonthlyExpense,
} = await import("../src/modules/expenses/expenses.service");

describe("schemas", () => {
  describe("createPeriodicExpenseSchema", () => {
    test("accepts valid input", () => {
      const result = createPeriodicExpenseSchema.parse({
        description: "Gimnasio",
        defaultDueDay: 10,
        defaultAmount: 1500.5,
        periods: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
      });
      expect(result.description).toBe("Gimnasio");
    });

    test("rejects empty description", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "",
          defaultDueDay: 10,
          defaultAmount: 100,
          periods: [1],
        }),
      ).toThrow();
    });

    test("rejects description over 50 chars", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "a".repeat(51),
          defaultDueDay: 10,
          defaultAmount: 100,
          periods: [1],
        }),
      ).toThrow();
    });

    test("rejects due day < 1", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "Test",
          defaultDueDay: 0,
          defaultAmount: 100,
          periods: [1],
        }),
      ).toThrow();
    });

    test("rejects due day > 31", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "Test",
          defaultDueDay: 32,
          defaultAmount: 100,
          periods: [1],
        }),
      ).toThrow();
    });

    test("rejects non-positive amount", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "Test",
          defaultDueDay: 15,
          defaultAmount: 0,
          periods: [1],
        }),
      ).toThrow();
    });

    test("rejects empty periods", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "Test",
          defaultDueDay: 15,
          defaultAmount: 100,
          periods: [],
        }),
      ).toThrow();
    });

    test("rejects invalid period month", () => {
      expect(() =>
        createPeriodicExpenseSchema.parse({
          description: "Test",
          defaultDueDay: 15,
          defaultAmount: 100,
          periods: [0, 13],
        }),
      ).toThrow();
    });
  });

  describe("createNonPeriodicExpenseSchema", () => {
    test("accepts valid input", () => {
      const result = createNonPeriodicExpenseSchema.parse({
        description: "Ropa",
        amount: 2500,
        dueDate: "2026-06-15T00:00:00.000Z",
        walletId: 1,
      });
      expect(result.description).toBe("Ropa");
    });

    test("rejects negative amount", () => {
      expect(() =>
        createNonPeriodicExpenseSchema.parse({
          description: "Ropa",
          amount: -100,
          dueDate: "2026-06-15T00:00:00.000Z",
        }),
      ).toThrow();
    });
  });

  describe("payExpenseSchema", () => {
    test("accepts valid payment", () => {
      const result = payExpenseSchema.parse({
        amountPayed: 1400,
        paymentDate: "2026-06-10T00:00:00.000Z",
        walletId: 1,
      });
      expect(result.amountPayed).toBe(1400);
    });

    test("allows missing paymentDate", () => {
      const result = payExpenseSchema.parse({
        amountPayed: 1400,
        walletId: 1,
      });
      expect(result.amountPayed).toBe(1400);
      expect(result.paymentDate).toBeUndefined();
    });

    test("rejects zero amountPayed", () => {
      expect(() => payExpenseSchema.parse({ amountPayed: 0 })).toThrow();
    });
  });

  describe("updateExpenseSchema", () => {
    test("accepts valid input", () => {
      const result = updateExpenseSchema.parse({
        amount: 2500,
        dueDate: "2026-07-15T00:00:00.000Z",
      });
      expect(result.amount).toBe(2500);
      expect(result.dueDate).toBe("2026-07-15T00:00:00.000Z");
    });

    test("rejects non-positive amount", () => {
      expect(() =>
        updateExpenseSchema.parse({
          amount: 0,
          dueDate: "2026-07-15T00:00:00.000Z",
        }),
      ).toThrow();
    });

    test("rejects invalid dueDate", () => {
      expect(() =>
        updateExpenseSchema.parse({
          amount: 100,
          dueDate: "not-a-date",
        }),
      ).toThrow();
    });
  });
});

describe("buildDueDate", () => {
  test("creates valid date", () => {
    const date = buildDueDate(2026, 6, 15);
    expect(date.getUTCFullYear()).toBe(2026);
    expect(date.getUTCMonth()).toBe(5); // 0-indexed
    expect(date.getUTCDate()).toBe(15);
  });

  test("clamps day to last day of month when dueDay exceeds month length", () => {
    const date = buildDueDate(2026, 2, 31);
    expect(date.getUTCMonth()).toBe(1); // February (0-indexed)
    expect(date.getUTCDate()).toBe(28);
  });

  test("handles december correctly", () => {
    const date = buildDueDate(2026, 12, 31);
    expect(date.getUTCFullYear()).toBe(2026);
    expect(date.getUTCMonth()).toBe(11);
    expect(date.getUTCDate()).toBe(31);
  });

  test("handles leap year february", () => {
    const date = buildDueDate(2024, 2, 29);
    expect(date.getUTCFullYear()).toBe(2024);
    expect(date.getUTCMonth()).toBe(1);
    expect(date.getUTCDate()).toBe(29);
  });
});

describe("getCurrentYearMonthUTC", () => {
  test("returns valid year and month", () => {
    const { year, month } = getCurrentYearMonthUTC();
    expect(year).toBeGreaterThan(2020);
    expect(month).toBeGreaterThanOrEqual(1);
    expect(month).toBeLessThanOrEqual(12);
  });
});

describe("listPeriodicExpenses", () => {
  test("returns only non-deleted expenses ordered by createdAt desc", async () => {
    const mockItems = [
      { id: 1, description: "Gas", isDeleted: false, createdAt: new Date() },
    ];
    mockPrisma.periodicExpense.findMany.mockResolvedValueOnce(mockItems);

    const result = await listPeriodicExpenses();
    expect(result).toEqual(mockItems);
    expect(mockPrisma.periodicExpense.findMany).toHaveBeenCalledWith({
      where: { isDeleted: false },
      orderBy: { description: "asc" },
    });
  });
});

describe("createPeriodicExpense", () => {
  test("creates and detects current month applicability", async () => {
    const input = {
      description: "Gimnasio",
      defaultDueDay: 10,
      defaultAmount: 1500,
      periods: [6],
    };
    const created = {
      id: 1,
      ...input,
      isDeleted: false,
      createdAt: new Date(),
    };
    mockPrisma.periodicExpense.create.mockResolvedValueOnce(created);

    const result = await createPeriodicExpense(input);
    expect(result.id).toBe(1);
    // currentMonthApplicable depends on actual month; just verify truthy/falsy
    expect("currentMonthApplicable" in result).toBe(true);
  });
});

describe("updatePeriodicExpense", () => {
  test("updates and returns the record", async () => {
    const input = {
      description: "Gas actualizado",
      defaultDueDay: 5,
      defaultAmount: 2000,
      periods: [1, 7],
    };
    const updated = { id: 1, ...input, isDeleted: false };
    mockPrisma.periodicExpense.update.mockResolvedValueOnce(updated);

    const result = await updatePeriodicExpense(1, input);
    expect(result.description).toBe("Gas actualizado");
    expect(mockPrisma.periodicExpense.update).toHaveBeenCalledWith({
      where: { id: 1 },
      data: input,
    });
  });
});

describe("softDeletePeriodicExpense", () => {
  test("sets isDeleted to true", async () => {
    const deleted = { id: 1, isDeleted: true };
    mockPrisma.periodicExpense.update.mockResolvedValueOnce(deleted);

    const result = await softDeletePeriodicExpense(1);
    expect(result.isDeleted).toBe(true);
    expect(mockPrisma.periodicExpense.update).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { isDeleted: true },
    });
  });
});

describe("listExpenses", () => {
  test("returns all expenses when no status filter", async () => {
    mockPrisma.expense.findMany.mockResolvedValueOnce([]);
    await listExpenses({});
    expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
      where: {},
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: 0,
      take: 10,
    });
  });

  test("filters by status when provided", async () => {
    mockPrisma.expense.findMany.mockResolvedValueOnce([]);
    await listExpenses({ status: "PENDING" });
    expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
      where: { status: "PENDING" },
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: 0,
      take: 10,
    });
  });

  test("filters by periodicExpenseId when provided", async () => {
    mockPrisma.expense.findMany.mockResolvedValueOnce([]);
    await listExpenses({ periodicExpenseId: 3 });
    expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
      where: { periodicExpenseId: 3 },
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: 0,
      take: 10,
    });
  });

  test("filters by search query with case-insensitive contains", async () => {
    mockPrisma.expense.findMany.mockResolvedValueOnce([]);
    await listExpenses({ search: "Gim" });
    expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
      where: { description: { contains: "Gim", mode: "insensitive" } },
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: 0,
      take: 10,
    });
  });

  test("combines status, periodicExpenseId, and search filters", async () => {
    mockPrisma.expense.findMany.mockResolvedValueOnce([]);
    await listExpenses({
      status: "PENDING",
      periodicExpenseId: 1,
      search: "Agua",
    });
    expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
      where: {
        status: "PENDING",
        periodicExpenseId: 1,
        description: { contains: "Agua", mode: "insensitive" },
      },
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: 0,
      take: 10,
    });
  });
});

describe("createNonPeriodicExpense", () => {
  test("creates expense with PAYED status and sets amountPayed", async () => {
    const input = {
      description: "Ropa",
      amount: 2500,
      dueDate: "2026-06-15T00:00:00.000Z",
      walletId: 1,
    };
    const created = {
      id: 1,
      description: "Ropa",
      amount: 2500,
      amountPayed: 2500,
      status: PaymentStatus.PAYED,
      year: 2026,
      month: 6,
    };
    const wallet = { id: 1, balance: 10000 };
    mockPrisma.expense.create.mockResolvedValueOnce(created);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 7500 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await createNonPeriodicExpense(input);
    expect(result.status).toBe(PaymentStatus.PAYED);
    expect(result.amountPayed).toBe(2500);
    expect(mockPrisma.expense.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        description: "Ropa",
        amount: 2500,
        amountPayed: 2500,
        status: PaymentStatus.PAYED,
        paymentDate: expect.any(Date),
      }),
    });
  });
});

describe("payExpense", () => {
  test("updates status to PAYED with amountPayed and paymentDate", async () => {
    const expense = {
      id: 1,
      description: "Ropa",
      status: PaymentStatus.PENDING,
      amount: 1400,
      amountPayed: null,
      paymentDate: null,
    };
    const paidExpense = {
      id: 1,
      status: PaymentStatus.PAYED,
      amountPayed: 1400,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
    };
    const wallet = { id: 1, balance: 10000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockResolvedValueOnce(paidExpense);
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 8600 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await payExpense(1, {
      amountPayed: 1400,
      paymentDate: "2026-06-10T00:00:00.000Z",
      walletId: 1,
    });
    expect(result.status).toBe(PaymentStatus.PAYED);
    expect(result.amountPayed).toBe(1400);
  });

  test("defaults paymentDate to now when not provided", async () => {
    const expense = {
      id: 1,
      description: "Ropa",
      status: PaymentStatus.PENDING,
      amount: 1500,
      amountPayed: null,
      paymentDate: null,
    };
    const wallet = { id: 1, balance: 10000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({
        id: where.id,
        ...data,
        status: PaymentStatus.PAYED,
        amountPayed: data.amountPayed,
        paymentDate: data.paymentDate,
      }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 8500 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await payExpense(1, { amountPayed: 1500, walletId: 1 });
    expect(result.status).toBe(PaymentStatus.PAYED);
    expect(result.paymentDate).toBeInstanceOf(Date);
  });
});

describe("updateExpense", () => {
  test("updates amount and dueDate for a PENDING expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PENDING,
      amount: 1500,
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      amountPayed: null,
      paymentDate: null,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 2000,
      dueDate: "2026-07-20T00:00:00.000Z",
    });
    expect(result.amount).toBe(2000);
    expect(result.year).toBe(2026);
    expect(result.month).toBe(7);
  });

  test("allows updating a PAYED expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 1500,
      amountPayed: 1500,
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 2000,
      dueDate: "2026-07-20T00:00:00.000Z",
      amountPayed: 1800,
    });
    expect(result.amount).toBe(2000);
    expect(result.year).toBe(2026);
    expect(result.month).toBe(7);
    expect(result.amountPayed).toBe(1800);
  });

  test("reverting a PAYED expense to PENDING clears payment info", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 1500,
      amountPayed: 1500,
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 1500,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.status).toBe(PaymentStatus.PENDING);
  });

  test("sets amountPayed and paymentDate to null when reverting to PENDING", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 1500,
      amountPayed: 1500,
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 1500,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.amountPayed).toBeNull();
    expect(result.paymentDate).toBeNull();
  });
});

describe("generateExpensesForCurrentMonth", () => {
  test("skips existing expenses to avoid duplicates", async () => {
    const { month } = getCurrentYearMonthUTC();
    mockPrisma.periodicExpense.findMany.mockResolvedValueOnce([
      {
        id: 1,
        description: "Gas",
        defaultDueDay: 15,
        defaultAmount: 2000,
        periods: [month],
      },
    ]);
    mockPrisma.expense.findFirst.mockResolvedValueOnce({ id: 99 });

    const results = await generateExpensesForCurrentMonth();
    expect(results).toHaveLength(1);
    expect(results[0].status).toBe("already_exists");
    expect(mockPrisma.expense.create).not.toHaveBeenCalled();
  });

  test("creates expenses for applicable periodic expenses", async () => {
    const { month } = getCurrentYearMonthUTC();
    const periodic = {
      id: 1,
      description: "Gas",
      defaultDueDay: 15,
      defaultAmount: 2000,
      periods: [month],
    };
    mockPrisma.periodicExpense.findMany.mockResolvedValueOnce([periodic]);
    mockPrisma.expense.findFirst.mockResolvedValueOnce(null);
    mockPrisma.expense.create.mockResolvedValueOnce({
      id: 1,
      description: "Gas",
      periodicExpenseId: 1,
      amount: 2000,
      status: "PENDING",
    });

    const results = await generateExpensesForCurrentMonth();
    expect(results).toHaveLength(1);
    expect(results[0].status).toBe("created");
  });

  test("ignores deleted periodic expenses", async () => {
    mockPrisma.periodicExpense.findMany.mockResolvedValueOnce([]);

    const results = await generateExpensesForCurrentMonth();
    expect(results).toHaveLength(0);
    expect(mockPrisma.periodicExpense.findMany).toHaveBeenCalledWith({
      where: { isDeleted: false, periods: { has: expect.any(Number) } },
    });
  });
});

describe("generateMonthlyExpense", () => {
  test("creates expense for given periodic expense", async () => {
    const periodic = {
      id: 1,
      description: "Gas",
      defaultDueDay: 15,
      defaultAmount: 2000,
      periods: [6],
    };
    mockPrisma.periodicExpense.findUniqueOrThrow.mockResolvedValueOnce(
      periodic,
    );
    mockPrisma.expense.findFirst.mockResolvedValueOnce(null);
    mockPrisma.expense.create.mockResolvedValueOnce({
      id: 1,
      description: "Gas",
      periodicExpenseId: 1,
      amount: 2000,
      status: "PENDING",
    });

    const result = await generateMonthlyExpense(1);
    expect(result.id).toBe(1);
  });

  test("returns existing expense instead of duplicating", async () => {
    const existing = { id: 99, description: "Gas", periodicExpenseId: 1 };
    mockPrisma.periodicExpense.findUniqueOrThrow.mockResolvedValueOnce({
      id: 1,
      description: "Gas",
      defaultDueDay: 15,
      defaultAmount: 2000,
      periods: [6],
    });
    mockPrisma.expense.findFirst.mockResolvedValueOnce(existing);

    const result = await generateMonthlyExpense(1);
    expect(result.id).toBe(99);
    expect(result).toEqual(existing);
  });
});

describe("getBeloSellPriceForDate", () => {
  test("returns current BELO sell price when paymentDate is today", async () => {
    const today = new Date();
    mockPrisma.quote.findFirst.mockResolvedValueOnce({
      sell: "1500.00",
    });

    const result = await getBeloSellPriceForDate(today);
    expect(result).toBe(1500);
    expect(mockPrisma.quote.findFirst).toHaveBeenCalledWith({
      where: { type: "BELO" },
    });
  });

  test("returns null when no current BELO quote exists for today", async () => {
    mockPrisma.quote.findFirst.mockResolvedValueOnce(null);

    const result = await getBeloSellPriceForDate(new Date());
    expect(result).toBeNull();
  });

  test("returns historical BELO sell price closest to 17:00 for a past date", async () => {
    const pastDate = new Date("2026-06-05T10:00:00.000Z");
    const before = {
      sell: "1400.50",
      timeStamp: new Date("2026-06-05T16:50:00.000Z"),
    };
    const after = {
      sell: "1410.00",
      timeStamp: new Date("2026-06-05T17:05:00.000Z"),
    };
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce(before)
      .mockResolvedValueOnce(after);

    const result = await getBeloSellPriceForDate(pastDate);
    expect(result).toBe(1410);
    expect(mockPrisma.quoteHistory.findFirst).toHaveBeenCalledTimes(2);
  });

  test("returns the only available historical entry if only one exists", async () => {
    const pastDate = new Date("2026-06-05T10:00:00.000Z");
    const before = {
      sell: "1400.50",
      timeStamp: new Date("2026-06-05T16:50:00.000Z"),
    };
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce(before)
      .mockResolvedValueOnce(null);

    const result = await getBeloSellPriceForDate(pastDate);
    expect(result).toBe(1400.5);
  });

  test("returns null when no historical data exists for a past date", async () => {
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce(null);

    const result = await getBeloSellPriceForDate(
      new Date("2026-06-05T10:00:00.000Z"),
    );
    expect(result).toBeNull();
  });
});

describe("payExpense with BELO", () => {
  test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
    const expense = {
      id: 1,
      description: "Ropa",
      status: PaymentStatus.PENDING,
      amount: 15000,
      amountPayed: null,
      paymentDate: null,
    };
    const paymentDate = new Date("2026-06-10T00:00:00.000Z");
    const wallet = { id: 1, balance: 100000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce({
        sell: "1500.00",
        timeStamp: new Date("2026-06-10T17:00:00.000Z"),
      })
      .mockResolvedValueOnce(null);

    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({
        id: where.id,
        status: PaymentStatus.PAYED,
        amountPayed: data.amountPayed,
        paymentDate: data.paymentDate,
        beloPrice: data.beloPrice,
        usdcEquivalent: data.usdcEquivalent,
      }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await payExpense(1, {
      amountPayed: 15000,
      paymentDate: "2026-06-10T00:00:00.000Z",
      walletId: 1,
    });

    expect(result.beloPrice).toBe(1500);
    expect(result.usdcEquivalent).toBe(10);
  });

  test("does not set BELO fields when no quote is available", async () => {
    const expense = {
      id: 1,
      description: "Ropa",
      status: PaymentStatus.PENDING,
      amount: 15000,
      amountPayed: null,
      paymentDate: null,
    };
    const wallet = { id: 1, balance: 100000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce(null);

    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({
        id: where.id,
        status: PaymentStatus.PAYED,
        amountPayed: data.amountPayed,
        paymentDate: data.paymentDate,
      }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await payExpense(1, {
      amountPayed: 15000,
      paymentDate: "2026-06-10T00:00:00.000Z",
      walletId: 1,
    });

    expect(result.beloPrice).toBeUndefined();
    expect(result.usdcEquivalent).toBeUndefined();
  });
});

describe("createNonPeriodicExpense with BELO", () => {
  test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
    const dueDate = "2026-06-15T00:00:00.000Z";
    const wallet = { id: 1, balance: 100000, currency: "ARS" };
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce({
        sell: "1500.00",
        timeStamp: new Date("2026-06-15T17:00:00.000Z"),
      })
      .mockResolvedValueOnce(null);

    mockPrisma.expense.create.mockImplementationOnce(
      async ({ data }) => ({
        id: 1,
        description: data.description,
        amount: data.amount,
        amountPayed: data.amountPayed,
        status: PaymentStatus.PAYED,
        beloPrice: data.beloPrice,
        usdcEquivalent: data.usdcEquivalent,
      }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 70000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await createNonPeriodicExpense({
      description: "Ropa",
      amount: 30000,
      dueDate,
      walletId: 1,
    });

    expect(result.beloPrice).toBe(1500);
    expect(result.usdcEquivalent).toBe(20);
  });

  test("does not set BELO fields when no quote is available", async () => {
    const wallet = { id: 1, balance: 100000, currency: "ARS" };
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce(null);

    mockPrisma.expense.create.mockImplementationOnce(
      async ({ data }) => ({
        id: 1,
        description: data.description,
        amount: data.amount,
        amountPayed: data.amountPayed,
        status: PaymentStatus.PAYED,
      }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 97500 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await createNonPeriodicExpense({
      description: "Ropa",
      amount: 2500,
      dueDate: "2026-06-15T00:00:00.000Z",
      walletId: 1,
    });

    expect(result.beloPrice).toBeUndefined();
    expect(result.usdcEquivalent).toBeUndefined();
  });
});

describe("updateExpense with BELO", () => {
  test("recalculates BELO when amountPayed changes on a PAYED expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 1500,
      amountPayed: 1500,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);

    mockPrisma.quoteHistory.findFirst
      .mockResolvedValueOnce({
        sell: "1600.00",
        timeStamp: new Date("2026-06-10T17:00:00.000Z"),
      })
      .mockResolvedValueOnce(null);

    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 1500,
      dueDate: "2026-06-15T00:00:00.000Z",
      amountPayed: 1600,
    });

    expect(result.beloPrice).toBe(1600);
    expect(result.usdcEquivalent).toBe(1);
  });

  test("clears BELO fields when reverting a PAYED expense to PENDING", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 1500,
      amountPayed: 1500,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      beloPrice: 1500,
      usdcEquivalent: 1,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 1500,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.beloPrice).toBeNull();
    expect(result.usdcEquivalent).toBeNull();
  });

  test("does not query BELO when updating a PENDING expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PENDING,
      amount: 1500,
      amountPayed: null,
      paymentDate: null,
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 2000,
      dueDate: "2026-07-20T00:00:00.000Z",
    });

    expect(result.beloPrice).toBeUndefined();
    expect(mockPrisma.quote.findFirst).not.toHaveBeenCalled();
    expect(mockPrisma.quoteHistory.findFirst).not.toHaveBeenCalled();
  });
});

describe("updateExpense wallet adjustments", () => {
  test("creates WITHDRAWAL when amount increases on a PAYED expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 100000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 90000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 35000,
      dueDate: "2026-06-15T00:00:00.000Z",
    });

    expect(result.amount).toBe(35000);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: -10000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "WITHDRAWAL",
        amount: -10000,
        description: "Ajuste por edición de gasto",
      },
    });
  });

  test("creates DEPOSIT when amount decreases on a PAYED expense", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 35000,
      amountPayed: 35000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 25000,
      dueDate: "2026-06-15T00:00:00.000Z",
    });

    expect(result.amount).toBe(25000);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: 10000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "DEPOSIT",
        amount: 10000,
        description: "Ajuste por edición de gasto",
      },
    });
  });

  test("does not create wallet adjustment when amount is unchanged", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 25000,
      dueDate: "2026-06-15T00:00:00.000Z",
    });

    expect(result.amount).toBe(25000);
    expect(mockWalletUpdate).not.toHaveBeenCalled();
    expect(mockMovementCreate).not.toHaveBeenCalled();
  });

  test("does not create wallet adjustment when existing expense has no walletId", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 35000,
      dueDate: "2026-06-15T00:00:00.000Z",
    });

    expect(result.amount).toBe(35000);
    expect(mockWalletUpdate).not.toHaveBeenCalled();
    expect(mockMovementCreate).not.toHaveBeenCalled();
  });

  test("creates DEPOSIT refund when reverting PAYED expense to PENDING with walletId", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 100000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 25000,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.status).toBe(PaymentStatus.PENDING);
    expect(result.amountPayed).toBeNull();
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: 25000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "DEPOSIT",
        amount: 25000,
        description: "Anulación de gasto",
      },
    });
  });

  test("does not create refund when existing expense has no walletId for PENDING revert", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );

    const result = await updateExpense(1, {
      amount: 25000,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.status).toBe(PaymentStatus.PENDING);
    expect(mockWalletUpdate).not.toHaveBeenCalled();
    expect(mockMovementCreate).not.toHaveBeenCalled();
  });

  test("throws when wallet is not found during adjustment", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(null);

    expect(
      updateExpense(1, {
        amount: 35000,
        dueDate: "2026-06-15T00:00:00.000Z",
      }),
    ).rejects.toThrow("Billetera no encontrada");
  });

  test("only creates refund (not amount adjustment) when both amount and status change", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 100000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 35000,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.status).toBe(PaymentStatus.PENDING);
    expect(mockWalletUpdate).toHaveBeenCalledTimes(1);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: 25000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "DEPOSIT",
        amount: 25000,
        description: "Anulación de gasto",
      },
    });
  });

  test("uses amountPayed for refund when expense has different amountPayed and amount", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 30000,
      amountPayed: 28000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 103000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 30000,
      dueDate: "2026-06-15T00:00:00.000Z",
      status: "PENDING",
    });

    expect(result.status).toBe(PaymentStatus.PENDING);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: 28000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "DEPOSIT",
        amount: 28000,
        description: "Anulación de gasto",
      },
    });
  });

  test("creates WITHDRAWAL when amountPayed increases while amount stays the same", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 65000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 25000,
      dueDate: "2026-06-15T00:00:00.000Z",
      amountPayed: 35000,
    });

    expect(result.amount).toBe(25000);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: -10000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "WITHDRAWAL",
        amount: -10000,
        description: "Ajuste por edición de gasto",
      },
    });
  });

  test("creates adjustment based on amountPayed when both amount and amountPayed change", async () => {
    const existing = {
      id: 1,
      status: PaymentStatus.PAYED,
      amount: 25000,
      amountPayed: 25000,
      paymentDate: new Date("2026-06-10T00:00:00.000Z"),
      dueDate: new Date("2026-06-15T00:00:00.000Z"),
      year: 2026,
      month: 6,
      walletId: 1,
    };
    const wallet = { id: 1, balance: 75000, currency: "ARS" };
    mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
    mockWalletFindUnique.mockResolvedValueOnce(wallet);
    mockPrisma.expense.update.mockImplementationOnce(
      async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
    );
    mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 72000 });
    mockMovementCreate.mockResolvedValueOnce({ id: 1 });

    const result = await updateExpense(1, {
      amount: 30000,
      dueDate: "2026-06-15T00:00:00.000Z",
      amountPayed: 28000,
    });

    expect(result.amount).toBe(30000);
    expect(mockWalletUpdate).toHaveBeenCalledWith({
      where: { id: 1 },
      data: { balance: { increment: -3000 } },
    });
    expect(mockMovementCreate).toHaveBeenCalledWith({
      data: {
        walletId: 1,
        type: "WITHDRAWAL",
        amount: -3000,
        description: "Ajuste por edición de gasto",
      },
    });
  });
});
