import { ConflictException, ForbiddenException } from '@nestjs/common';
import { RoleEnum } from '../roles/roles.enum';
import { StatusEnum } from '../statuses/statuses.enum';
import { AdminUsersService } from './admin-users.service';

describe('AdminUsersService', () => {
  const team = { id: 10, name: 'Alpha', alias: 'alpha' };
  const teamRole = { id: 1, name: 'Player' };
  const actorId = 1;
  let target: any;
  let activeAdmins: any[];
  let player: any;
  let playerRows: any[];
  let playerTotal: number;
  let lockQuery: any;
  let lockedUserQuery: any;
  let playerQuery: any;
  let lockedPlayerQuery: any;
  let userRepository: any;
  let playerRepository: any;
  let manager: any;
  let dataSource: any;
  let logger: any;
  let service: AdminUsersService;

  beforeEach(() => {
    target = user(2, RoleEnum.user, StatusEnum.active);
    activeAdmins = [user(actorId, RoleEnum.admin, StatusEnum.active)];
    player = assignment(101, null);
    playerRows = [];
    playerTotal = 0;
    lockQuery = chain({ getMany: jest.fn(() => activeAdmins) });
    lockedUserQuery = chain({ getOne: jest.fn(() => target) });
    playerQuery = chain({
      getRawMany: jest.fn(() => playerRows),
      getCount: jest.fn(() => playerTotal),
    });
    lockedPlayerQuery = chain({ getOne: jest.fn(() => player) });
    userRepository = {
      createQueryBuilder: jest.fn((alias: string) =>
        alias === 'lockedUser' ? lockedUserQuery : lockQuery,
      ),
      findOne: jest.fn(() => target),
      save: jest.fn((value) => Promise.resolve(value)),
    };
    playerRepository = {
      createQueryBuilder: jest.fn((alias: string) =>
        alias === 'lockedPlayer' ? lockedPlayerQuery : playerQuery,
      ),
      findOne: jest.fn(() => player),
      save: jest.fn((value) => Promise.resolve(value)),
    };
    manager = {
      getRepository: jest.fn((entity) =>
        entity.name === 'User' ? userRepository : playerRepository,
      ),
    };
    dataSource = {
      manager,
      transaction: jest.fn((work) => work(manager)),
    };
    logger = { info: jest.fn() };
    service = new AdminUsersService(dataSource, logger);
  });

  it('updates only names and returns an explicit Task 1-compatible safe admin summary', async () => {
    target.players = [assignment(101, target)];

    const result = await service.updateProfile(actorId, target.id, {
      firstName: 'New',
      lastName: 'Name',
    });

    expect(userRepository.save).toHaveBeenCalledWith(
      expect.objectContaining({ firstName: 'New', lastName: 'Name' }),
    );
    expect(result).toEqual({
      id: 2,
      firstName: 'New',
      lastName: 'Name',
      email: 'user-2@example.com',
      role: { id: RoleEnum.user, name: 'User' },
      status: { id: StatusEnum.active, name: 'Active' },
      assignments: [playerSummary(101)],
    });
    expect(result).not.toHaveProperty('password');
    expect(result).not.toHaveProperty('hash');
    expect(result).not.toHaveProperty('socialId');
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'admin_user_profile_update',
        details: 'targetUserId=2',
        userId: actorId,
      },
      manager,
    );
    expect(lockedUserQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(lockedUserQuery.leftJoinAndSelect).toHaveBeenCalledTimes(2);
  });

  it('rejects self-demotion and self-deactivation before saving', async () => {
    target = user(actorId, RoleEnum.admin, StatusEnum.active);

    await expect(
      service.updateRole(actorId, actorId, RoleEnum.user),
    ).rejects.toBeInstanceOf(ForbiddenException);
    await expect(
      service.updateStatus(actorId, actorId, StatusEnum.inactive),
    ).rejects.toBeInstanceOf(ForbiddenException);
    expect(userRepository.save).not.toHaveBeenCalled();
  });

  it('locks the active-admin set and rejects loss of the last active admin', async () => {
    target = user(2, RoleEnum.admin, StatusEnum.active);
    activeAdmins = [target];

    await expect(
      service.updateRole(actorId, target.id, RoleEnum.user),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(lockQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(dataSource.transaction).toHaveBeenCalledTimes(1);
    expect(userRepository.save).not.toHaveBeenCalled();
  });

  it('allows a locked role change when another active admin remains', async () => {
    target = user(2, RoleEnum.admin, StatusEnum.active);
    activeAdmins = [target, user(3, RoleEnum.admin, StatusEnum.active)];

    const result = await service.updateRole(actorId, target.id, RoleEnum.user);

    expect(target.role).toEqual({ id: RoleEnum.user });
    expect(result.role).toEqual({ id: RoleEnum.user, name: undefined });
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'admin_user_role_update',
        details: 'targetUserId=2 roleId=2',
        userId: actorId,
      },
      manager,
    );
  });

  it('deactivates under the same lock without changing player assignments', async () => {
    target = user(2, RoleEnum.user, StatusEnum.active);
    target.players = [assignment(101, target)];

    const result = await service.updateStatus(
      actorId,
      target.id,
      StatusEnum.inactive,
    );

    expect(target.status).toEqual({ id: StatusEnum.inactive });
    expect(target.hash).toBeNull();
    expect(result.assignments).toEqual([playerSummary(101)]);
    expect(playerRepository.save).not.toHaveBeenCalled();
  });

  it('returns an explicit paginated player projection with safe current-user summaries', async () => {
    playerRows = [
      {
        player_id: 101,
        first_name: 'Pat',
        last_name: 'Player',
        active: 1,
        team_id: 10,
        team_name: 'Alpha',
        team_alias: 'alpha',
        user_id: 2,
        user_first_name: 'Target',
        user_last_name: 'User',
        status_id: StatusEnum.active,
        status_name: 'Active',
      },
    ];
    playerTotal = 1;

    const result = await service.findPlayers({
      search: 'pat',
      teamId: 10,
      assignment: 'assigned',
      page: 1,
      limit: 20,
    });

    expect(result).toEqual({
      data: [
        {
          id: 101,
          firstName: 'Pat',
          lastName: 'Player',
          active: true,
          team,
          currentUser: {
            id: 2,
            firstName: 'Target',
            lastName: 'User',
            status: { id: StatusEnum.active, name: 'Active' },
          },
        },
      ],
      page: 1,
      limit: 20,
      total: 1,
      hasNextPage: false,
    });
    expect(playerQuery.andWhere).toHaveBeenCalledWith(
      'player.teamId = :teamId',
      {
        teamId: 10,
      },
    );
    expect(playerQuery.andWhere).toHaveBeenCalledWith(
      'player.userId IS NOT NULL',
    );
  });

  it('atomically reassigns a locked player and returns the refreshed target summary', async () => {
    const previous = user(9, RoleEnum.user, StatusEnum.active);
    player = assignment(101, previous);
    target.players = [player];

    const result = await service.assignPlayer(actorId, target.id, player.id);

    expect(dataSource.transaction).toHaveBeenCalledTimes(1);
    expect(lockedPlayerQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(player.user).toBe(target);
    expect(playerRepository.save).toHaveBeenCalledWith(player);
    expect(result.id).toBe(target.id);
    expect(logger.info).toHaveBeenCalledWith(
      {
        event: 'admin_player_assign',
        details: 'targetUserId=2 playerId=101 previousUserId=9',
        userId: actorId,
      },
      manager,
    );
  });

  it('unlinks only when the locked player is currently linked to the target user', async () => {
    player = assignment(101, user(9, RoleEnum.user, StatusEnum.active));

    await expect(
      service.unlinkPlayer(actorId, target.id, player.id),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(playerRepository.save).not.toHaveBeenCalled();

    player.user = target;
    player.userId = target.id;
    await service.unlinkPlayer(actorId, target.id, player.id);
    expect(player.user).toBeNull();
    expect(playerRepository.save).toHaveBeenCalledWith(player);
  });

  function user(id: number, roleId: RoleEnum, statusId: StatusEnum) {
    return {
      id,
      firstName: id === 2 ? 'Target' : 'Admin',
      lastName: id === 2 ? 'User' : String(id),
      email: `user-${id}@example.com`,
      password: `password-${id}`,
      hash: `hash-${id}`,
      socialId: `social-${id}`,
      role: {
        id: roleId,
        name: roleId === RoleEnum.admin ? 'Admin' : 'User',
      },
      status: {
        id: statusId,
        name: statusId === StatusEnum.active ? 'Active' : 'Inactive',
      },
      players: [],
    };
  }

  function assignment(id: number, assignedUser: any) {
    return {
      id,
      firstName: 'Pat',
      lastName: 'Player',
      active: true,
      team,
      teamRole,
      user: assignedUser,
      userId: assignedUser?.id ?? null,
    };
  }

  function playerSummary(id: number) {
    return {
      id,
      firstName: 'Pat',
      lastName: 'Player',
      active: true,
      team,
      teamRole,
    };
  }

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