import { ForbiddenException } from '@nestjs/common';
import { AuthProvidersEnum } from './auth-providers.enum';
import { AuthService } from './auth.service';
import { RoleEnum } from '../roles/roles.enum';
import { StatusEnum } from '../statuses/statuses.enum';

describe('AuthService inactive-user enforcement and safe logging', () => {
  let jwtService: any;
  let usersService: any;
  let logger: any;
  let dataSource: any;
  let confirmationUser: any;
  let lockedUserQuery: any;
  let userRepository: any;
  let service: AuthService;
  let mailService: any;

  beforeEach(() => {
    jwtService = {
      sign: jest.fn(() => 'signed-token'),
      verify: jest.fn(),
      decode: jest.fn(),
    };
    usersService = {
      findOne: jest.fn(),
      update: jest.fn(),
      create: jest.fn(),
      linkPlayerToUserId: jest.fn(),
    };
    logger = { info: jest.fn(), debug: jest.fn() };
    mailService = { userSignUp: jest.fn() };
    confirmationUser = user(StatusEnum.inactive);
    confirmationUser.hash = 'confirmation-hash';
    lockedUserQuery = {
      where: jest.fn().mockReturnThis(),
      setLock: jest.fn().mockReturnThis(),
      getOne: jest.fn(() => confirmationUser),
    };
    userRepository = {
      createQueryBuilder: jest.fn(() => lockedUserQuery),
      save: jest.fn((value) => Promise.resolve(value)),
    };
    const manager = { getRepository: jest.fn(() => userRepository) };
    dataSource = {
      transaction: jest.fn((work) => work(manager)),
    };
    service = new AuthService(
      jwtService,
      usersService,
      {} as any,
      mailService,
      logger,
      dataSource,
      { assertAtLeast: jest.fn() } as any,
    );
  });

  it('rejects password login for an inactive user before issuing a token', async () => {
    usersService.findOne.mockResolvedValue(user(StatusEnum.inactive));

    await expect(
      service.validateLogin({
        email: 'inactive@example.com',
        password: 'password',
      }),
    ).rejects.toBeInstanceOf(ForbiddenException);

    expect(jwtService.sign).not.toHaveBeenCalled();
    expect(logger.info).toHaveBeenCalledWith({
      event: 'user_login_fail',
      details: 'userId=2 reason=inactive',
      userId: 2,
    });
  });

  it('never includes an email in an unknown-user login audit event', async () => {
    usersService.findOne.mockResolvedValue(undefined);

    await expect(
      service.validateLogin({
        email: 'secret@example.com',
        password: 'secret-password',
      }),
    ).rejects.toBeDefined();

    expect(logger.info).toHaveBeenCalledWith({
      event: 'user_login_fail',
      details: 'reason=user_not_found',
      userId: -1,
    });
    expect(JSON.stringify(logger.info.mock.calls)).not.toContain(
      'secret@example.com',
    );
    expect(JSON.stringify(logger.info.mock.calls)).not.toContain(
      'secret-password',
    );
  });

  it('never includes a rejected invite token in logging details', async () => {
    jwtService.verify.mockImplementation(() => {
      throw new Error('invalid');
    });

    await expect(
      service.getTeamFromInvite('secret-token'),
    ).rejects.toBeDefined();

    expect(logger.info).toHaveBeenCalledWith({
      event: 'user_invite_link_validate_fail',
      details: 'invitation validation failed',
      userId: 0,
    });
    expect(JSON.stringify(logger.info.mock.calls)).not.toContain(
      'secret-token',
    );
  });

  it('does not refresh an inactive user through the me endpoint flow', async () => {
    jwtService.verify.mockReturnValue({ id: 2 });
    usersService.findOne.mockResolvedValue(user(StatusEnum.inactive));

    await expect(service.me('Bearer existing-token')).rejects.toBeInstanceOf(
      ForbiddenException,
    );
    expect(jwtService.sign).not.toHaveBeenCalled();
  });

  it('serializes email confirmation on the user row and consumes the hash', async () => {
    await service.confirmEmail('confirmation-hash');

    expect(dataSource.transaction).toHaveBeenCalledTimes(1);
    expect(lockedUserQuery.setLock).toHaveBeenCalledWith('pessimistic_write');
    expect(confirmationUser.status).toEqual({ id: StatusEnum.active });
    expect(confirmationUser.hash).toBeNull();
    expect(userRepository.save).toHaveBeenCalledWith(confirmationUser);
  });

  it('ignores a public registration player id and never mutates player ownership', async () => {
    usersService.create.mockResolvedValue({
      id: 8,
      email: 'new@example.com',
    });

    await service.register({
      email: 'new@example.com',
      password: 'password',
      firstName: 'New',
      lastName: 'User',
      linkPlayerId: 101,
    } as any);

    expect(usersService.create.mock.calls[0][0]).not.toHaveProperty(
      'linkPlayerId',
    );
    expect(usersService.linkPlayerToUserId).not.toHaveBeenCalled();
  });

  function user(statusId: StatusEnum) {
    return {
      id: 2,
      email: 'inactive@example.com',
      password: 'password-hash',
      provider: AuthProvidersEnum.email,
      role: { id: RoleEnum.user, name: 'User' },
      status: {
        id: statusId,
        name: statusId === StatusEnum.active ? 'Active' : 'Inactive',
      },
    };
  }
});
