import { ConflictException, NotFoundException } from '@nestjs/common';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { TransactionTypeEnum } from '../transactions/transaction-type.enum';
import {
  DEACTIVATION_ADJUSTMENT_NOTE_PREFIX,
  TeamMembersService,
} from './team-members.service';

describe('TeamMembersService', () => {
  const teamId = 10;
  let player: any;
  let treasurers: any[];
  let treasurerLockQuery: any;
  let lockedPlayerQuery: any;
  let playerRepository: any;
  let transactionRepository: any;
  let manager: any;
  let dataSource: any;
  let logger: any;
  let access: any;
  let service: TeamMembersService;

  beforeEach(() => {
    player = makePlayer(101, true, TeamRolesEnum.treasurer, 0);
    treasurers = [player];
    treasurerLockQuery = chain({ getMany: jest.fn(() => treasurers) });
    lockedPlayerQuery = chain({ getOne: jest.fn(() => player) });
    playerRepository = {
      createQueryBuilder: jest.fn((alias: string) =>
        alias === 'lockedPlayer' ? lockedPlayerQuery : treasurerLockQuery,
      ),
      save: jest.fn((value) => Promise.resolve(value)),
      findOne: jest.fn(() => player),
    };
    transactionRepository = {
      insert: jest.fn(() => Promise.resolve({ identifiers: [{ id: 999 }] })),
      find: jest.fn(() => Promise.resolve([])),
    };
    manager = {
      getRepository: jest.fn((entity) =>
        entity.name === 'Player' ? playerRepository : transactionRepository,
      ),
    };
    dataSource = { transaction: jest.fn((work) => work(manager)) };
    logger = { info: jest.fn() };
    access = { assertAtLeast: jest.fn(() => Promise.resolve()) };
    service = new TeamMembersService(dataSource, logger, access as any);
  });

  it('checks the team-manager permission before touching the database', async () => {
    access.assertAtLeast.mockRejectedValue(new Error('forbidden'));
    await expect(service.setActive(5, teamId, player.id, false)).rejects.toThrow(
      'forbidden',
    );
    expect(dataSource.transaction).not.toHaveBeenCalled();
  });

  it('rejects deactivating the last active treasurer and writes nothing', async () => {
    player.balance = 42;
    treasurers = [player];

    await expect(
      service.setActive(5, teamId, player.id, false),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(treasurerLockQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(transactionRepository.insert).not.toHaveBeenCalled();
    expect(playerRepository.save).not.toHaveBeenCalled();
    expect(logger.info).not.toHaveBeenCalled();
  });

  it('allows deactivation when another active treasurer remains, and zeroes the balance via a credit transaction', async () => {
    player.balance = 42;
    treasurers = [player, makePlayer(102, true, TeamRolesEnum.treasurer, 0)];

    const result = await service.setActive(5, teamId, player.id, false);

    expect(transactionRepository.insert).toHaveBeenCalledWith(
      expect.objectContaining({
        amount: -42,
        note: expect.stringContaining(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX),
        type: { id: TransactionTypeEnum.credit },
      }),
    );
    expect(playerRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({ active: false, balance: 0 }),
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'player_active_update',
        details: `teamId=${teamId} playerId=${player.id} active=false`,
        userId: 5,
      },
      manager,
    );
    expect(result.active).toBe(false);
  });

  it('deactivating a non-treasurer with zero balance never inserts an adjustment transaction', async () => {
    player = makePlayer(101, true, TeamRolesEnum.player, 0);
    lockedPlayerQuery = chain({ getOne: jest.fn(() => player) });
    playerRepository.createQueryBuilder = jest.fn((alias: string) =>
      alias === 'lockedPlayer' ? lockedPlayerQuery : treasurerLockQuery,
    );
    treasurers = [];

    await expect(
      service.setActive(5, teamId, player.id, false),
    ).resolves.toBeDefined();
    expect(transactionRepository.insert).not.toHaveBeenCalled();
  });

  it('recomputes balance on reactivation, excluding deactivation adjustment rows across multiple cycles', async () => {
    player = makePlayer(101, false, TeamRolesEnum.player, 0);
    lockedPlayerQuery = chain({ getOne: jest.fn(() => player) });
    playerRepository.createQueryBuilder = jest.fn((alias: string) =>
      alias === 'lockedPlayer' ? lockedPlayerQuery : treasurerLockQuery,
    );
    transactionRepository.find.mockResolvedValue([
      { amount: 50, type: { id: TransactionTypeEnum.credit }, note: 'Zahlung' },
      { amount: -20, note: `${DEACTIVATION_ADJUSTMENT_NOTE_PREFIX} #101` },
      { amount: 10, type: { id: TransactionTypeEnum.credit }, note: 'Zahlung 2' },
      { amount: -40, note: `${DEACTIVATION_ADJUSTMENT_NOTE_PREFIX} #101` },
      { amount: 15, type: { id: 11 }, note: 'Strafe' },
    ]);

    const result = await service.setActive(5, teamId, player.id, true);

    // erwarteter Saldo: 50 + 10 - 15 = 45 (beide Ausgleichsbuchungen ausgeschlossen)
    expect(playerRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({ active: true, balance: 45 }),
    );
    expect(result.balance).toBe(45);
  });

  it('no-ops when the requested active state already matches (idempotent, no writes)', async () => {
    player = makePlayer(101, true, TeamRolesEnum.player, 0);
    lockedPlayerQuery = chain({ getOne: jest.fn(() => player) });
    playerRepository.createQueryBuilder = jest.fn((alias: string) =>
      alias === 'lockedPlayer' ? lockedPlayerQuery : treasurerLockQuery,
    );

    await service.setActive(5, teamId, player.id, true);

    expect(playerRepository.save).not.toHaveBeenCalled();
    expect(logger.info).not.toHaveBeenCalled();
  });

  it('rejects changing team-role away from treasurer for the last active treasurer', async () => {
    treasurers = [player];

    await expect(
      service.setTeamRole(5, teamId, player.id, TeamRolesEnum.captain),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(playerRepository.save).not.toHaveBeenCalled();
  });

  it('allows a team-role change when another active treasurer remains, and never touches balance/transactions', async () => {
    treasurers = [player, makePlayer(102, true, TeamRolesEnum.treasurer, 0)];

    const result = await service.setTeamRole(
      5,
      teamId,
      player.id,
      TeamRolesEnum.captain,
    );

    expect(transactionRepository.insert).not.toHaveBeenCalled();
    expect(playerRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({ teamRole: { id: TeamRolesEnum.captain } }),
    );
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'player_team_role_update',
        details: `teamId=${teamId} playerId=${player.id} teamRoleId=${TeamRolesEnum.captain}`,
        userId: 5,
      },
      manager,
    );
    expect(result.teamRole).toEqual({ id: TeamRolesEnum.captain });
  });

  it('throws NotFoundException when the locked player does not belong to the given team', async () => {
    player.team = { id: 999 };

    await expect(
      service.setActive(5, teamId, player.id, false),
    ).rejects.toBeInstanceOf(NotFoundException);
  });

  function makePlayer(
    id: number,
    active: boolean,
    teamRoleId: number,
    balance: number,
  ) {
    return {
      id,
      firstName: 'Pat',
      lastName: 'Player',
      active,
      balance,
      team: { id: teamId },
      teamRole: { id: teamRoleId },
    };
  }

  function chain(overrides: Record<string, jest.Mock>) {
    const query: Record<string, jest.Mock> = {};
    [
      'innerJoin',
      'innerJoinAndSelect',
      'leftJoinAndSelect',
      'where',
      'andWhere',
      'setLock',
      'orderBy',
    ].forEach((method) => (query[method] = jest.fn(() => query)));
    return Object.assign(query, overrides);
  }
});
