import { BadRequestException } from '@nestjs/common';
import { WalletsService } from './wallets.service';
import { Bill } from '../entities/bill.entity';

const manager = (walletBalance: number) => {
  const wallet = { id: 1, studentId: 10, balance: walletBalance };
  const saved: any[] = [];
  return {
    wallet,
    saved,
    value: {
      findOne: jest.fn(async () => wallet),
      findOneByOrFail: jest.fn(async () => wallet),
      save: jest.fn(async (value: any) => { saved.push(value); return value; }),
      create: jest.fn((_entity: unknown, value: unknown) => value),
      createQueryBuilder: jest.fn(),
    },
  };
};

describe('WalletsService payment rules', () => {
  const service = new WalletsService({} as any, {} as any, {} as any, {} as any);

  it('partially pays a bill when balance is insufficient', async () => {
    const ctx = manager(40);
    const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 0, outstandingAmount: 100, status: 'unpaid' } as Bill;
    const result = await service.debitBill(ctx.value as any, bill, 1);
    expect(result.status).toBe('partially_paid');
    expect(Number(result.paidAmount)).toBe(40);
    expect(Number(result.outstandingAmount)).toBe(60);
    expect(Number(ctx.wallet.balance)).toBe(0);
    expect(ctx.saved.some((row) => row.type === 'bill_payment' && Number(row.amount) === -40)).toBe(true);
  });

  it('marks a bill paid when balance covers it', async () => {
    const ctx = manager(120);
    const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 0, outstandingAmount: 100, status: 'unpaid' } as Bill;
    const result = await service.debitBill(ctx.value as any, bill);
    expect(result.status).toBe('paid');
    expect(Number(result.outstandingAmount)).toBe(0);
    expect(Number(ctx.wallet.balance)).toBe(20);
  });

  it('refunds paid amount and cancels the bill', async () => {
    const ctx = manager(10);
    const bill = { id: 9, studentId: 10, totalAmount: 100, paidAmount: 40, outstandingAmount: 60, status: 'partially_paid' } as Bill;
    const result = await service.refundBill(ctx.value as any, bill, '录入错误', 1);
    expect(result.status).toBe('cancelled');
    expect(Number(ctx.wallet.balance)).toBe(50);
    expect(ctx.saved.some((row) => row.type === 'bill_refund' && Number(row.amount) === 40)).toBe(true);
  });
});

describe('WalletsService financial boundaries', () => {
  it('caps a debit at total minus already paid even when outstandingAmount is stale', async () => {
    const ctx = manager(50);
    const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
    const bill = {
      id: 10,
      studentId: 10,
      totalAmount: 100,
      paidAmount: 90,
      outstandingAmount: 100,
      status: 'partially_paid',
    } as Bill;

    await service.debitBill(ctx.value as any, bill);

    expect(bill).toMatchObject({ paidAmount: 100, outstandingAmount: 0, status: 'paid' });
    expect(ctx.wallet.balance).toBe(40);
    expect(ctx.saved.some((row) => row.type === 'bill_payment' && row.amount === -10)).toBe(true);
  });

  it('caps a refund at the bill total when paidAmount is corrupt', async () => {
    const ctx = manager(10);
    const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
    const bill = {
      id: 11,
      studentId: 10,
      totalAmount: 100,
      paidAmount: 150,
      outstandingAmount: 0,
      status: 'paid',
    } as Bill;

    await service.refundBill(ctx.value as any, bill, '冲正');

    expect(ctx.wallet.balance).toBe(110);
    expect(ctx.saved.some((row) => row.type === 'bill_refund' && row.amount === 100)).toBe(true);
  });

  it('does not issue a second refund for an already cancelled bill', async () => {
    const ctx = manager(10);
    const service = new WalletsService({} as any, {} as any, {} as any, {} as any);
    const bill = {
      id: 12,
      studentId: 10,
      totalAmount: 100,
      paidAmount: 100,
      outstandingAmount: 0,
      status: 'cancelled',
    } as Bill;

    await service.refundBill(ctx.value as any, bill, '重复取消');

    expect(ctx.wallet.balance).toBe(10);
    expect(ctx.saved).toHaveLength(0);
  });

  it('rejects an amount that rounds to zero before opening a transaction', async () => {
    const dataSource = { transaction: jest.fn() };
    const service = new WalletsService({} as any, {} as any, {} as any, dataSource as any);

    await expect(service.changeBalance({ studentId: 1, amount: 0.004, type: 'adjustment' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });
});

describe('WalletsService wallet locking', () => {
  const createQueryManager = () => {
    const wallet = { id: 1, studentId: 10, balance: 0 };
    const query = {
      where: jest.fn(),
      setLock: jest.fn(),
      getOne: jest.fn(async () => wallet),
    };
    query.where.mockReturnValue(query);
    query.setLock.mockReturnValue(query);
    return {
      wallet,
      query,
      manager: {
        createQueryBuilder: jest.fn(() => query),
        findOne: jest.fn(),
      },
    };
  };

  it('skips pessimistic locking for SQLite', async () => {
    const ctx = createQueryManager();
    const service = new WalletsService(
      {} as any,
      {} as any,
      {} as any,
      { options: { type: 'better-sqlite3' } } as any,
    );

    const result = await (service as any).getOrCreateWallet(ctx.manager, 10, true);

    expect(result).toBe(ctx.wallet);
    expect(ctx.query.setLock).not.toHaveBeenCalled();
    expect(ctx.query.getOne).toHaveBeenCalled();
  });

  it('keeps pessimistic write locking for MySQL', async () => {
    const ctx = createQueryManager();
    const service = new WalletsService(
      {} as any,
      {} as any,
      {} as any,
      { options: { type: 'mysql' } } as any,
    );

    await (service as any).getOrCreateWallet(ctx.manager, 10, true);

    expect(ctx.query.setLock).toHaveBeenCalledWith('pessimistic_write');
  });
});
