import { BadRequestException, NotFoundException } from '@nestjs/common';
import { ExpensesService } from './expenses.service';
import { PersonalExpense } from '../entities/personal-expense.entity';

const qb = (affected = 1) => ({
  delete: jest.fn().mockReturnThis(),
  where: jest.fn().mockReturnThis(),
  execute: jest.fn().mockResolvedValue({ affected }),
});

function createService(options?: {
  roomFind?: any[];
  personalFind?: any[];
  roomExpense?: any;
  personalExpense?: any;
}) {
  const roomExpRepo = {
    create: jest.fn((value) => value),
    save: jest.fn(async (value) => value),
    find: jest.fn().mockResolvedValue(options?.roomFind ?? []),
    findOne: jest.fn().mockResolvedValue(options?.roomExpense ?? null),
    createQueryBuilder: jest.fn(() => qb()),
  };
  const personalExpRepo = {
    create: jest.fn((value) => value),
    save: jest.fn(async (value) => value),
    find: jest.fn().mockResolvedValue(options?.personalFind ?? []),
    findOne: jest.fn().mockResolvedValue(options?.personalExpense ?? null),
    createQueryBuilder: jest.fn(() => qb()),
    delete: jest.fn(),
  };
  const roomRepo = {
    find: jest.fn().mockImplementation(async () => options?.roomFind ?? []),
    findOne: jest.fn().mockResolvedValue({ id: 1 }),
  };
  const studentRepo = { findOne: jest.fn().mockResolvedValue({ id: 1 }) };
  return {
    service: new ExpensesService(roomExpRepo as any, personalExpRepo as any, roomRepo as any, studentRepo as any, {} as any),
    roomExpRepo,
    personalExpRepo,
    roomRepo,
  };
}

describe('ExpensesService boundaries', () => {
  it('rejects an empty room-expense batch', async () => {
    const { service, roomExpRepo } = createService();
    await expect(service.batchCreateRoomExpenses({
      periodStart: '2026-07-01',
      periodEnd: '2026-07-31',
      expenses: [],
    })).rejects.toBeInstanceOf(BadRequestException);
    expect(roomExpRepo.save).not.toHaveBeenCalled();
  });

  it('rejects a batch when any room does not exist', async () => {
    const { service, roomExpRepo } = createService({ roomFind: [{ id: 1 }] });
    await expect(service.batchCreateRoomExpenses({
      periodStart: '2026-07-01',
      periodEnd: '2026-07-31',
      expenses: [
        { roomId: 1, expenseType: 'water', amount: 10 },
        { roomId: 2, expenseType: 'water', amount: 20 },
      ],
    })).rejects.toBeInstanceOf(NotFoundException);
    expect(roomExpRepo.save).not.toHaveBeenCalled();
  });

  it('rejects a reversed room-expense period', async () => {
    const { service } = createService();
    await expect(service.createRoomExpense({
      roomId: 1,
      expenseType: 'water',
      amount: 10,
      periodStart: '2026-08-01',
      periodEnd: '2026-07-31',
    })).rejects.toBeInstanceOf(BadRequestException);
  });

  it('rejects a batch delete when only part of the ids exist', async () => {
    const { service, roomExpRepo } = createService({ roomFind: [{ id: 1 }] });
    await expect(service.batchDeleteRoomExpenses([1, 2])).rejects.toBeInstanceOf(NotFoundException);
    expect(roomExpRepo.createQueryBuilder).not.toHaveBeenCalled();
  });

  it('does not edit or delete a personal expense already linked to a bill', async () => {
    const linked = { id: 1, studentId: 1, amount: 20, billId: 9 } as PersonalExpense;
    const { service, personalExpRepo } = createService({ personalExpense: linked });

    await expect(service.updatePersonalExpense(1, { amount: 30 })).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.deletePersonalExpense(1)).rejects.toBeInstanceOf(BadRequestException);
    expect(personalExpRepo.save).not.toHaveBeenCalled();
    expect(personalExpRepo.delete).not.toHaveBeenCalled();
  });

  it('rejects a personal-expense batch delete containing billed records', async () => {
    const { service, personalExpRepo } = createService({
      personalFind: [
        { id: 1, billId: null },
        { id: 2, billId: 9 },
      ],
    });

    await expect(service.batchDeletePersonalExpenses([1, 2])).rejects.toBeInstanceOf(BadRequestException);
    expect(personalExpRepo.createQueryBuilder).not.toHaveBeenCalled();
  });

  describe('batchImportPersonalExpenses', () => {
    it('rejects zero amount', async () => {
      const { service } = createService();
      const result = await service.batchImportPersonalExpenses([
        {
          studentName: '张三',
          expenseType: '其他',
          amount: 0,
          expenseDate: '2026-07-01',
          description: '测试',
        },
      ]);
      expect(result.imported).toBe(0);
      expect(result.skipped).toBe(1);
      expect(result.errors).toBeDefined();
      expect(result.errors![0]).toContain('费用金额必须大于0');
    });

    it('rejects negative amount', async () => {
      const { service } = createService();
      const result = await service.batchImportPersonalExpenses([
        {
          studentName: '张三',
          expenseType: '其他',
          amount: -100,
          expenseDate: '2026-07-01',
          description: '测试',
        },
      ]);
      expect(result.imported).toBe(0);
      expect(result.skipped).toBe(1);
      expect(result.errors).toBeDefined();
      expect(result.errors![0]).toContain('费用金额必须大于0');
    });

    it('rejects NaN amount', async () => {
      const { service } = createService();
      const result = await service.batchImportPersonalExpenses([
        {
          studentName: '张三',
          expenseType: '其他',
          amount: NaN,
          expenseDate: '2026-07-01',
          description: '测试',
        },
      ]);
      expect(result.imported).toBe(0);
      expect(result.skipped).toBe(1);
      expect(result.errors).toBeDefined();
      expect(result.errors![0]).toContain('费用金额最多保留两位小数');
    });

    it('allows valid positive amount', async () => {
      const { service, personalExpRepo } = createService();
      const result = await service.batchImportPersonalExpenses([
        {
          studentName: '张三',
          expenseType: '其他',
          amount: 500,
          expenseDate: '2026-07-01',
          description: '测试',
        },
      ]);
      expect(result.imported).toBe(1);
      expect(result.skipped).toBe(0);
      expect(personalExpRepo.save).toHaveBeenCalled();
    });

    it('allows fractional amount with two decimal places', async () => {
      const { service, personalExpRepo } = createService();
      const result = await service.batchImportPersonalExpenses([
        {
          studentName: '张三',
          expenseType: '其他',
          amount: 99.99,
          expenseDate: '2026-07-01',
          description: '测试',
        },
      ]);
      expect(result.imported).toBe(1);
      expect(result.skipped).toBe(0);
      expect(personalExpRepo.save).toHaveBeenCalled();
    });
  });

  describe('batchImportUtilityExpenses', () => {
    it('rejects reversed period (end before start)', async () => {
      const { service } = createService();
      const result = await service.batchImportUtilityExpenses([
        {
          periodStr: '2026-02-08 ~ 2026-01-21',
          roomNumber: '101',
          electricityAmount: 100,
          electricityFee: 50,
          waterAmount: 0,
          waterFee: 0,
          totalFee: 50,
        },
      ]);
      expect(result.imported).toBe(0);
      expect(result.skipped).toBe(1);
      expect(result.errors).toBeDefined();
      expect(result.errors![0]).toContain('账期无效');
    });
  });
});
