import { ConflictException, NotFoundException } from '@nestjs/common';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { PenaltyService } from './penalty.service';

describe('PenaltyService 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 duplicateQuery = {
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    getCount: jest.fn(),
  };
  const teamRepository = { createQueryBuilder: jest.fn(() => teamQuery) };
  const writeRepository = {
    createQueryBuilder: jest.fn(() => duplicateQuery),
    create: jest.fn((value) => value),
    save: jest.fn(),
    findOne: jest.fn(),
    remove: jest.fn(),
  };
  const manager = {
    getRepository: jest.fn((entity) =>
      entity.name === 'Team' ? teamRepository : writeRepository,
    ),
  };
  const dataSource = { transaction: jest.fn((work) => work(manager)) };
  const access = { assertMember: jest.fn(), assertAtLeast: jest.fn() };
  const logger = { info: jest.fn() };
  let service: PenaltyService;

  beforeEach(() => {
    jest.clearAllMocks();
    teamQuery.getOne.mockResolvedValue({ id: 5 });
    duplicateQuery.getCount.mockResolvedValue(0);
    teamReadRepository.findOne.mockResolvedValue({ id: 5 });
    service = new PenaltyService(
      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 catalog', async () => {
    teamReadRepository.findOne.mockResolvedValue(null);

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

  it('keeps the authenticated cross-team catalog route compatible and safely mapped', async () => {
    readRepository.find.mockResolvedValue([
      {
        id: 8,
        description: 'Zu spät',
        amount: '5.50',
        createdAt: new Date('2026-01-02T00:00:00.000Z'),
        team: { id: 5, secret: 'hidden' },
      },
    ]);

    await expect(service.getUserPenalties(42)).resolves.toEqual([
      {
        id: 8,
        description: 'Zu spät',
        amount: 5.5,
        createdAt: new Date('2026-01-02T00:00:00.000Z'),
      },
    ]);
    expect(readRepository.find).toHaveBeenCalledWith({
      where: {
        team: { players: { user: { id: 42 }, active: true } },
      },
      order: { description: 'ASC' },
    });
  });

  it('authorizes team reads, sorts them, and maps only safe fields', async () => {
    readRepository.find.mockResolvedValue([
      {
        id: 8,
        description: 'Zu spät',
        amount: '5.50',
        createdAt: new Date('2026-01-02T00:00:00.000Z'),
        team: { id: 5, secret: 'hidden' },
      },
    ]);

    await expect(service.getTeamPenalties(42, 5)).resolves.toEqual([
      {
        id: 8,
        description: 'Zu spät',
        amount: 5.5,
        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 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.createPenalty(
        { teamId: 5, description: '  Handy in der Kabine  ', amount: 3.25 },
        42,
      ),
    ).resolves.toMatchObject({
      id: 9,
      description: 'Handy in der Kabine',
      amount: 3.25,
    });

    expect(teamQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(access.assertAtLeast).toHaveBeenCalledWith(
      42,
      5,
      'penalty_manage_min_role',
      TeamRolesEnum.captain,
      manager,
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'penalty_catalog_create',
        details: 'teamId=5 penaltyId=9 action=create',
        userId: 42,
      },
      manager,
    );
  });

  it('rejects a normalized duplicate before writing or auditing', async () => {
    duplicateQuery.getCount.mockResolvedValue(1);

    await expect(
      service.createPenalty(
        { teamId: 5, description: '  ZU SPÄT ', amount: 5 },
        42,
      ),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(writeRepository.save).not.toHaveBeenCalled();
    expect(logger.info).not.toHaveBeenCalled();
  });

  it('updates an entry in its owning team and excludes itself from duplicate detection', async () => {
    readRepository.findOne.mockResolvedValue({ id: 8, team: { id: 5 } });
    writeRepository.findOne.mockResolvedValue({
      id: 8,
      team: { id: 5 },
      description: 'Alt',
      amount: 1,
      createdAt: new Date('2026-01-02T00:00:00.000Z'),
    });
    writeRepository.save.mockImplementation(async (value) => value);

    await expect(
      service.updatePenalty(8, { description: '  Neu  ', amount: 2.5 }, 42),
    ).resolves.toMatchObject({ id: 8, description: 'Neu', amount: 2.5 });
    expect(duplicateQuery.andWhere).toHaveBeenCalledWith(
      'penalty.id != :penaltyId',
      { penaltyId: 8 },
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'penalty_catalog_update',
        details: 'teamId=5 penaltyId=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 penalty = { id: 8, team: { id: 5 } };
    writeRepository.findOne.mockResolvedValue(penalty);

    await service.deletePenalty(8, 42);

    expect(writeRepository.remove).toHaveBeenCalledWith(penalty);
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'penalty_catalog_delete',
        details: 'teamId=5 penaltyId=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.deletePenalty(999, 42)).rejects.toBeInstanceOf(
      NotFoundException,
    );
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });

  it('rejects the mutation when transactional audit persistence fails', async () => {
    writeRepository.save.mockResolvedValue({
      id: 9,
      team: { id: 5 },
      description: 'Neu',
      amount: 2,
      createdAt: new Date(),
    });
    logger.info.mockRejectedValue(new Error('audit unavailable'));

    await expect(
      service.createPenalty({ teamId: 5, description: 'Neu', amount: 2 }, 42),
    ).rejects.toThrow('audit unavailable');
  });
});
