import { NotFoundException } from '@nestjs/common';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { TransactionTypeEnum } from '../transactions/transaction-type.enum';
import { RecurringTransactionIntervalEnum } from './recurring-transaction-interval.enum';
import { RecurringTransactionsService } from './recurring-transactions.service';

describe('RecurringTransactionsService catalog management', () => {
  const readRepository = { find: jest.fn(), findOne: jest.fn() };
  const teamReadRepository = { findOne: jest.fn() };
  const teamQuery = {
    where: jest.fn().mockReturnThis(),
    setLock: jest.fn().mockReturnThis(),
    getOne: jest.fn(),
  };
  const teamRepository = { createQueryBuilder: jest.fn(() => teamQuery) };
  const typeRepository = { findOne: jest.fn() };
  const writeRepository = {
    create: jest.fn((value) => value),
    save: jest.fn(),
    findOne: jest.fn(),
    remove: jest.fn(),
  };
  const manager = {
    getRepository: jest.fn((entity) => {
      if (entity.name === 'Team') return teamRepository;
      if (entity.name === 'TransactionType') return typeRepository;
      return writeRepository;
    }),
  };
  const dataSource = { transaction: jest.fn((work) => work(manager)) };
  const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() };
  const logger = { info: jest.fn() };
  let service: RecurringTransactionsService;

  beforeEach(() => {
    jest.clearAllMocks();
    teamQuery.getOne.mockResolvedValue({ id: 5 });
    teamReadRepository.findOne.mockResolvedValue({ id: 5 });
    typeRepository.findOne.mockResolvedValue({
      id: TransactionTypeEnum.fee,
      name: 'fee',
    });
    service = new RecurringTransactionsService(
      readRepository as any,
      teamReadRepository as any,
      dataSource as any,
      access as any,
      logger as any,
    );
  });

  it('returns not found instead of leaking an unknown team as an empty list', async () => {
    teamReadRepository.findOne.mockResolvedValue(null);

    await expect(
      service.getTeamRecurringTransactions(42, 999),
    ).rejects.toBeInstanceOf(NotFoundException);
    expect(access.assertMember).not.toHaveBeenCalled();
  });

  it('authorizes team reads, sorts them, and maps only safe fields', async () => {
    readRepository.find.mockResolvedValue([
      {
        id: 8,
        description: 'Monatsbeitrag',
        amount: '10.00',
        type: { id: TransactionTypeEnum.fee, name: 'fee' },
        interval: RecurringTransactionIntervalEnum.monthly,
        nextRunDate: '2026-09-01T00:00:00.000Z',
        active: true,
        createdAt: new Date('2026-01-02T00:00:00.000Z'),
        team: { id: 5, secret: 'hidden' },
      },
    ]);

    await expect(
      service.getTeamRecurringTransactions(42, 5),
    ).resolves.toEqual([
      {
        id: 8,
        description: 'Monatsbeitrag',
        amount: 10,
        type: TransactionTypeEnum.fee,
        interval: RecurringTransactionIntervalEnum.monthly,
        nextRunDate: '2026-09-01T00:00:00.000Z',
        active: true,
        createdAt: new Date('2026-01-02T00:00:00.000Z'),
      },
    ]);
    expect(access.assertMember).toHaveBeenCalledWith(42, 5);
    expect(readRepository.find).toHaveBeenCalledWith({
      where: { team: { id: 5 } },
      order: { description: 'ASC' },
    });
  });

  it('creates a normalized entry under a team lock, resolves the type, and audits in the transaction', async () => {
    writeRepository.save.mockImplementation(async (value) => ({
      id: 9,
      createdAt: new Date('2026-01-03T00:00:00.000Z'),
      ...value,
    }));

    await expect(
      service.createRecurringTransaction(
        {
          teamId: 5,
          description: '  Monatsbeitrag  ',
          amount: 10,
          type: TransactionTypeEnum.fee,
          interval: RecurringTransactionIntervalEnum.monthly,
          startDate: '2026-09-01T00:00:00.000Z',
        },
        42,
      ),
    ).resolves.toMatchObject({
      id: 9,
      description: 'Monatsbeitrag',
      amount: 10,
      type: TransactionTypeEnum.fee,
      nextRunDate: '2026-09-01T00:00:00.000Z',
      active: true,
    });

    expect(teamQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(access.assertAtLeast).toHaveBeenCalledWith(
      42,
      5,
      'transaction_create_min_role',
      TeamRolesEnum.scnd_treasurer,
      manager,
    );
    expect(typeRepository.findOne).toHaveBeenCalledWith({
      where: { id: TransactionTypeEnum.fee },
    });
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'recurring_transaction_create',
        details: 'teamId=5 recurringTransactionId=9 action=create',
        userId: 42,
      },
      manager,
    );
  });

  it('rejects an unknown transaction type before writing or auditing', async () => {
    typeRepository.findOne.mockResolvedValue(null);

    await expect(
      service.createRecurringTransaction(
        {
          teamId: 5,
          description: 'Monatsbeitrag',
          amount: 10,
          type: TransactionTypeEnum.fee,
          interval: RecurringTransactionIntervalEnum.monthly,
          startDate: '2026-09-01T00:00:00.000Z',
        },
        42,
      ),
    ).rejects.toBeInstanceOf(NotFoundException);
    expect(writeRepository.save).not.toHaveBeenCalled();
    expect(logger.info).not.toHaveBeenCalled();
  });

  it('updates fields in the owning team without touching nextRunDate, and audits it', async () => {
    readRepository.findOne.mockResolvedValue({ id: 8, team: { id: 5 } });
    writeRepository.findOne.mockResolvedValue({
      id: 8,
      team: { id: 5 },
      description: 'Alt',
      amount: 5,
      type: { id: TransactionTypeEnum.levy },
      interval: RecurringTransactionIntervalEnum.monthly,
      nextRunDate: '2026-09-01T00:00:00.000Z',
      active: true,
      createdAt: new Date('2026-01-02T00:00:00.000Z'),
    });
    writeRepository.save.mockImplementation(async (value) => value);

    await expect(
      service.updateRecurringTransaction(
        8,
        {
          description: '  Neu  ',
          amount: 12,
          type: TransactionTypeEnum.fee,
          interval: RecurringTransactionIntervalEnum.quarterly,
          active: false,
        },
        42,
      ),
    ).resolves.toMatchObject({
      id: 8,
      description: 'Neu',
      amount: 12,
      type: TransactionTypeEnum.fee,
      interval: RecurringTransactionIntervalEnum.quarterly,
      active: false,
      nextRunDate: '2026-09-01T00:00:00.000Z',
    });
    expect(access.assertAtLeast).toHaveBeenCalledWith(
      42,
      5,
      'transaction_create_min_role',
      TeamRolesEnum.scnd_treasurer,
      manager,
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'recurring_transaction_update',
        details: 'teamId=5 recurringTransactionId=8 action=update',
        userId: 42,
      },
      manager,
    );
  });

  it('deletes an entry permanently after manager authorization and audits it', async () => {
    readRepository.findOne.mockResolvedValue({ id: 8, team: { id: 5 } });
    const entry = { id: 8, team: { id: 5 } };
    writeRepository.findOne.mockResolvedValue(entry);

    await service.deleteRecurringTransaction(8, 42);

    expect(writeRepository.remove).toHaveBeenCalledWith(entry);
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'recurring_transaction_delete',
        details: 'teamId=5 recurringTransactionId=8 action=delete',
        userId: 42,
      },
      manager,
    );
  });

  it('returns not found when a mutation target does not exist', async () => {
    readRepository.findOne.mockResolvedValue(null);

    await expect(
      service.deleteRecurringTransaction(999, 42),
    ).rejects.toBeInstanceOf(NotFoundException);
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });
});
