import 'reflect-metadata';
import { validate } from 'class-validator';
import { ConflictException, NotFoundException } from '@nestjs/common';
import { CreateExpenseTypeDto, UpdateExpenseTypeDto } from './dto/expense-type.dto';
import { ExpenseTypesService } from './expense-types.service';

describe('ExpenseTypesService boundaries', () => {
  const createService = () => {
    const repo = {
      findOne: jest.fn(),
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => ({ id: 1, ...value })),
      remove: jest.fn(),
    };
    return { service: new ExpenseTypesService(repo as never), repo };
  };

  it('normalizes code and name before duplicate detection and save', async () => {
    const { service, repo } = createService();
    repo.findOne.mockResolvedValue(null);
    await service.create({ code: ' water ', name: ' 水费 ' });
    expect(repo.findOne).toHaveBeenCalledWith({ where: { code: 'water' } });
    expect(repo.create).toHaveBeenCalledWith({ code: 'water', name: '水费' });
  });

  it('rejects a normalized duplicate code', async () => {
    const { service, repo } = createService();
    repo.findOne.mockResolvedValue({ id: 1, code: 'water' });
    await expect(service.create({ code: ' water ', name: '水费' }))
      .rejects.toBeInstanceOf(ConflictException);
  });

  it('trims an updated name and preserves omitted fields', async () => {
    const { service, repo } = createService();
    repo.findOne.mockResolvedValue({ id: 1, code: 'water', name: '旧名称', enabled: true });
    await service.update(1, { name: ' 新名称 ' });
    expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({
      code: 'water', name: '新名称', enabled: true,
    }));
  });

  it('rejects removal of a missing type', async () => {
    const { service, repo } = createService();
    repo.findOne.mockResolvedValue(null);
    await expect(service.remove(999)).rejects.toBeInstanceOf(NotFoundException);
    expect(repo.remove).not.toHaveBeenCalled();
  });
});

describe('expense type DTO boundaries', () => {
  it.each(['Water', '1water', 'water-fee', '', 'a'.repeat(31)])('rejects code %j', async (code) => {
    const dto = Object.assign(new CreateExpenseTypeDto(), { code, name: '水费' });
    expect((await validate(dto)).some((error) => error.property === 'code')).toBe(true);
  });

  it.each(['', '   '])('rejects blank name %j and negative sort order', async (name) => {
    const dto = Object.assign(new CreateExpenseTypeDto(), {
      code: 'water', name, sortOrder: -1,
    });
    const properties = (await validate(dto)).map((error) => error.property);
    expect(properties).toEqual(expect.arrayContaining(['name', 'sortOrder']));
  });

  it('accepts zero sort order and boolean enabled update', async () => {
    const dto = Object.assign(new UpdateExpenseTypeDto(), { sortOrder: 0, enabled: false });
    expect(await validate(dto)).toEqual([]);
  });
});
