import { TransactionTypeEnum } from '../transactions/transaction-type.enum';
import { RecurringTransactionIntervalEnum } from './recurring-transaction-interval.enum';
import { RecurringTransactionsScheduler } from './recurring-transactions.scheduler';

describe('RecurringTransactionsScheduler', () => {
  const dueRepository = { find: jest.fn() };
  const playerRepository = { find: jest.fn() };
  const definitionWriteRepository = { save: jest.fn((value) => value) };
  const transactionWriteRepository = {
    create: jest.fn((value) => value),
    save: jest.fn(async (value) => ({ id: 1, ...value })),
  };
  const manager = {
    getRepository: jest.fn((entity) => {
      if (entity.name === 'Player') return playerRepository;
      if (entity.name === 'Transaction') return transactionWriteRepository;
      return definitionWriteRepository;
    }),
  };
  const dataSource = { transaction: jest.fn((work) => work(manager)) };
  const logger = { info: jest.fn() };
  let scheduler: RecurringTransactionsScheduler;

  const player = (id: number, active: boolean) => ({ id, active, team: { id: 5 } });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  function buildScheduler() {
    return new RecurringTransactionsScheduler(
      dueRepository as any,
      dataSource as any,
      logger as any,
    );
  }

  it('does nothing when no recurring transaction is due', async () => {
    dueRepository.find.mockResolvedValue([]);
    scheduler = buildScheduler();

    await scheduler.runDueRecurringTransactions();

    expect(dataSource.transaction).not.toHaveBeenCalled();
    expect(logger.info).not.toHaveBeenCalled();
  });

  it('books a transaction for every active player and skips inactive ones', async () => {
    dueRepository.find.mockResolvedValue([
      {
        id: 3,
        team: { id: 5 },
        description: 'Monatsbeitrag',
        amount: 10,
        type: { id: TransactionTypeEnum.fee },
        interval: RecurringTransactionIntervalEnum.monthly,
        nextRunDate: '2026-08-01T00:00:00.000Z',
        active: true,
      },
    ]);
    // The DB query filters by active:true itself (asserted below) — an
    // inactive player would never be returned, so the mock reflects that.
    playerRepository.find.mockResolvedValue([player(1, true), player(3, true)]);
    scheduler = buildScheduler();

    await scheduler.runDueRecurringTransactions();

    expect(playerRepository.find).toHaveBeenCalledWith({
      where: { team: { id: 5 }, active: true },
    });
    expect(transactionWriteRepository.save).toHaveBeenCalledTimes(2);
    expect(transactionWriteRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        amount: 10,
        note: 'Monatsbeitrag',
        player: player(1, true),
        type: { id: TransactionTypeEnum.fee },
      }),
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'recurring_transaction_run',
        details:
          'recurringTransactionId=3 teamId=5 gebuchteSpieler=2',
        userId: -1,
      },
      manager,
    );
  });

  it.each([
    [RecurringTransactionIntervalEnum.monthly, '2026-08-01T00:00:00.000Z', '2026-09-01T00:00:00.000Z'],
    [RecurringTransactionIntervalEnum.quarterly, '2026-08-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z'],
    [RecurringTransactionIntervalEnum.yearly, '2026-08-01T00:00:00.000Z', '2027-08-01T00:00:00.000Z'],
  ])(
    'advances nextRunDate by %s from %s to %s',
    async (interval, nextRunDate, expected) => {
      dueRepository.find.mockResolvedValue([
        {
          id: 3,
          team: { id: 5 },
          description: 'Monatsbeitrag',
          amount: 10,
          type: { id: TransactionTypeEnum.fee },
          interval,
          nextRunDate,
          active: true,
        },
      ]);
      playerRepository.find.mockResolvedValue([player(1, true)]);
      scheduler = buildScheduler();

      await scheduler.runDueRecurringTransactions();

      expect(definitionWriteRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ nextRunDate: expected }),
      );
    },
  );

  it('processes each due definition inside its own transaction', async () => {
    dueRepository.find.mockResolvedValue([
      {
        id: 3,
        team: { id: 5 },
        description: 'A',
        amount: 10,
        type: { id: TransactionTypeEnum.fee },
        interval: RecurringTransactionIntervalEnum.monthly,
        nextRunDate: '2026-08-01T00:00:00.000Z',
        active: true,
      },
      {
        id: 4,
        team: { id: 6 },
        description: 'B',
        amount: 20,
        type: { id: TransactionTypeEnum.levy },
        interval: RecurringTransactionIntervalEnum.yearly,
        nextRunDate: '2026-08-01T00:00:00.000Z',
        active: true,
      },
    ]);
    playerRepository.find.mockResolvedValue([player(1, true)]);
    scheduler = buildScheduler();

    await scheduler.runDueRecurringTransactions();

    expect(dataSource.transaction).toHaveBeenCalledTimes(2);
  });
});
