import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import { TetardCoinService } from './tetard-coin.service';
import { TetardCoin } from './entities/tetard-coin.entity';
import { Transaction } from './entities/transaction.entity';

const makeTcRepo = () => ({
  findOne: jest.fn(),
  create: jest.fn((dto) => ({ ...dto })),
  save: jest.fn(async (entity) => entity),
});

const makeTxRepo = () => ({
  create: jest.fn((dto) => ({ ...dto })),
  save: jest.fn(async (entity) => entity),
});

describe('TetardCoinService', () => {
  let service: TetardCoinService;
  let tcRepo: ReturnType<typeof makeTcRepo>;
  let txRepo: ReturnType<typeof makeTxRepo>;

  beforeEach(async () => {
    tcRepo = makeTcRepo();
    txRepo = makeTxRepo();
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TetardCoinService,
        { provide: getRepositoryToken(TetardCoin), useValue: tcRepo },
        { provide: getRepositoryToken(Transaction), useValue: txRepo },
      ],
    }).compile();
    service = module.get<TetardCoinService>(TetardCoinService);
  });

  describe('earn()', () => {
    it('augmente la balance (nouvel utilisateur)', async () => {
      tcRepo.findOne.mockResolvedValue(null);
      expect(await service.earn('u1', 10)).toBe(10);
    });

    it('augmente la balance (utilisateur existant)', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 50 });
      expect(await service.earn('u1', 30)).toBe(80);
    });

    it('crée une entrée transaction audit', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 0 });
      await service.earn('u1', 20, 'subscription-t1');
      expect(txRepo.save).toHaveBeenCalledTimes(1);
      expect(txRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ userId: 'u1', amount: 20, reason: 'subscription-t1' }),
      );
    });

    it('throw si amount <= 0', async () => {
      await expect(service.earn('u1', 0)).rejects.toThrow(BadRequestException);
      await expect(service.earn('u1', -5)).rejects.toThrow(BadRequestException);
    });
  });

  describe('spend()', () => {
    it('diminue la balance du montant correct', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 100 });
      expect(await service.spend('u1', 30, 'endurance')).toBe(70);
    });

    it('throw si balance insuffisante (invariant ≥ 0)', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 10 });
      await expect(service.spend('u1', 15, 'guild')).rejects.toThrow(BadRequestException);
    });

    it('throw si balance = 0', async () => {
      tcRepo.findOne.mockResolvedValue(null);
      await expect(service.spend('u1', 1, 'cosmetic')).rejects.toThrow(BadRequestException);
    });

    it('crée une transaction négative (audit)', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 50 });
      await service.spend('u1', 20, 'forge');
      expect(txRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ amount: -20, reason: 'forge' }),
      );
    });
  });

  describe('getBalance()', () => {
    it('retourne 0 si aucun record', async () => {
      tcRepo.findOne.mockResolvedValue(null);
      expect(await service.getBalance('u1')).toBe(0);
    });

    it('retourne la balance correcte', async () => {
      tcRepo.findOne.mockResolvedValue({ userId: 'u1', balance: 42 });
      expect(await service.getBalance('u1')).toBe(42);
    });
  });
});
